状压DP学习个人心得
状态压缩DP,顾名思义,在处理大部分棋盘问题时,我们将其某一系列状态进行压缩,并对其进行遍历寻找需要的结果。
对于棋盘状态的压缩,以一行(或一列)为单位,通常采用二进制来表示其的一种状态,以一个5列的棋盘中的一行为例:
列 | 4 | 3 | 2 | 1 | 0 |
状态 | 使用 | 未用 | 使用 | 未用 | 未用 |
二进制 | 1 | 0 | 1 | 0 | 0 |
对于一个5列的棋盘,若为空(即不摆放任何棋子),状态为:0 0 0 0 0(二进制),0(十进制);若全部摆放棋子,状态为:1 1 1 1 1(二进制),31(十进制)
在这种算法要求每一个单元的状态只有两种,使用和未使用(即可以用0和1来表示),那么对于一行中所有列的状态,我们用1、0来表示,则对于此行的这种特定状态,从左到右(或从右到左)可将其视为一个二进制数,不难想到对于一行的所有可能的状态都可以用一个二进制数唯一的表示,然后以十进制的形式存储。这样用一个二进制数来表示一行的状态之后,就可用简单的位运算而不是循环来判断一行中每一列的状态,同时,一行的所有状态不超过种(n为列的数量)
到此可以想到,其实状压DP是另一种暴力遍历所有可能性求解的方法,所以棋盘的行列大概是在20行以内;
既然我们需要遍历每一种状态,就代表需要先知道所有的状态,也就是之前提到的用二进制表示,十进制存储的方法。首先遍历一行的每种状态,对其进行编号,之后每一行的合法状态都是这个状态的子集
int col=5; //还是以之前的5列棋盘为例
int maxx = (1<<(col+1))+10; //这一行中有5列,那么状态最多有2^6-1种状态;
int state[maxx]; //以十进制的方式存储所有状态
int num; // 用于记录状态编号
for(int i=0;i<maxx;i++)
{
// 如果i状态是我们想要的,那么记录
state[num++] = i; // i状态的编号为num
}
// 对于状态的记录也可以写成state[++num] = i; 区别就是标号从1开始而不是0(我看大佬都是从1开始的,不知道是个人习惯还是有特殊原因,暂时不知道)
由此可得出递推公式,对于第i行的某种状态,形成这种状态k的方案数=∑第i-1行所有状态的方案数;
dp[i][j] = ∑dp[[i-1][0~k] ( k∈[0,len(state[])] )
以 “牧场的安排” 为例 (P1879 [USACO06NOV]Corn Fields G)
- 题目描述
农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
- 输入格式
第一行:两个整数M和N,用空格隔开。
第2到第M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。第i+1行描述了第i行的土地,所有整数均为0或1,是1的话,表示这块土地足够肥沃,0则表示这块土地不适合种草。
- 输出格式
一个整数,即牧场分配总方案数除以100,000,000的余数。
- 输入样例
2 3
1 1 1
0 1 0
- 输出样例
9
此题有两个限制条件:①要求任意两块草坪不能有公共边,也就是任意一块草坪的上下左右都不能有草坪。
第 i-1 行 | 1 | 0 | 0 | 1 | 0 |
第 i 行 | 0 | 1 | 0 | 0 | 1 |
第 i+1 行 | 0 | 0 | 1 | 0 | 0 |
②草不种在贫瘠的土地上;
对于条件①,可按相邻两行来判断:对于第i行j列,若此格种草,则此格行内左右都没有种草且第i-1行的j列也没有种草,也就是说对于第i行的状态state[x],将其与state[x]>>1,state[x]<<1分别按位与后都为0则满足条件;对于条件②,只需要将其与题目一开始给出的状态mmap[i]按位后若不变则满足条件;
设置dp[i][j]数组,i戴白到第i行(包含i行)为止的状态,当前行,也就是i行的状态编号,dp[i][j]代表对于第到第i行为止(包含第i行)的k状态有多少种方案数
思路1:
#include<bits/stdc++.h>
#define ll long long
#define inf 999999
using namespace std;
ll dp[20][4196];
bool state[4196];
ll mmap[20];
ll ans,num;
int n,m;
int main()
{
cin >>n>>m;
for(int i=1;i<=n;i++)
{
ll b;
for(int j=1;j<=m;j++){
cin>>b;
mmap[i] =(mmap[i]<<1) + b; // 将读入数据按二进制存储
}
}
for(int i=0;i<(1<<m);i++)
{
state[i] = ((i&(i<<1))==0 && (i&(i>>1))==0); // 保存行间合法状态
}
dp[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<(1<<m);j++)
if(state[j] && ((j&mmap[i])==j)) // 保证第i行和第i-1行状态合法且满足条件二
for(int k=0;k<(1<<m);k++)
if((k&j)==0)
dp[i][j] = (dp[i][j]+dp[i-1][k])%100000000; // 状态转移
for(int i=0;i<(1<<m);i++)
ans += dp[n][i],ans %= 100000000;
printf("%lld\n",ans);
return 0;
}
这个思路将所有行间合法状态存储后直接对所有状态进行遍历,个人认为:i行的状态最大即为mmap[i],在之前就对每行的何方状态先进行保存就能省去部分循环状态
思路2:
#include<bits/stdc++.h>
#define ll long long
#define inf 999999
using namespace std;
ll dp[20][4196];
ll mmap[20];
ll ans,num;
int n,m;
vector<int> state[20];
int main()
{
cin >>n>>m;
for(int i=1;i<=n;i++)
{
ll b;
for(int j=1;j<=m;j++)
{
cin>>b;
mmap[i] =(mmap[i]<<1) + b;
}
for(int j=0;j<=mmap[i];j++)
{
if( (j&(j<<1)) || (j&(j>>1)) || (j&(mmap[i]))!=j) continue;
if(i==1) state[0].push_back(j); // 将第0行状态与第1行设置相同,方便第1行进行转态转移
state[i].push_back(j);
}
}
dp[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=0;j<state[i].size();j++)
for(int k=0;k<state[i-1].size();k++)
if((state[i][j]&state[i-1][k])==0) //若第0行没有状态,会导致第1行没有状态可转移
dp[i][j] = (dp[i][j]+dp[i-1][k])%100000000;
for(int i=0;i<(1<<m);i++)
ans += dp[n][i],ans %= 100000000;
printf("%lld\n",ans);
return 0;
}
这里除了先设置第0行的状态和第1行的相同之外,还可以可以先将第一行所有可行方案数 dp[i][0~k] 设置为1;
for(int i=1;i<state[1].size();i++)
dp[1][i]=1;
for(int i=2;i<=n;i++)
for(int j=0;j<state[i].size();j++)
for(int k=0;k<state[i-1].size();k++)
if((state[i][j]&state[i-1][k])==0)
dp[i][j] = (dp[i][j]+dp[i-1][k])%100000000;