状压入门--bzoj1087: [SCOI2005]互不侵犯King【状压dp】
Description
在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上
左下右上右下八个方向上附近的各一个格子,共8个格子。
Input
只有一行,包含两个数N,K ( 1 <=N <=9, 0 <= K <= N * N)
Output
方案数。
Sample Input
3 2
Sample Output
16
简单的题目描述,往往蕴藏着巧妙的算法--X
这道题的数据范围十分清新,暗喻着这道题可以承受较高的复杂度。
由于在棋盘上,我们可以把1当做放国王,0当做不放国王。那么整个矩阵就是一个01矩阵。
分解来看,每一行(我们通常从行的角度考虑)就是一个01串。
然而我们又知道,枚举01串的复杂度是指数级的,就算我们数据范围小,也犯不得枚举次数多呀!
如果我们用dp来做这道题,那本题的状态一定大的一匹,对dp进行优化可从两方面考虑:状态和转移。转移目测布星,我们考虑压缩状态。
沿着上面的思路,如果我们把棋盘当做01矩阵,每一行当做01串,那这个01串就可以看做一个二进制数,我们的状态就是这个二进制对应的十进制数。于是我们就完美地进行了状态压缩。
举个栗子:
0101 假设表示在第一行的第2、4列放国王,那么状态我们就可以表示为5,因为101(2)表示十进制的5.
那么,我们可以预处理出这些状态。而为了满足“互不侵犯”的性质,我们可以在进行状压dp预处理时就处理掉不合法的一些状态,留下合理的。
那么,我们可以把国王的攻击分为两类:同行的、不同行的,现在我们就要留下同行的合法解。因为一个国王会攻击它左右直接相邻的国王,所以合法的一行01串中“1”一定不能连续出现。
void getnowk(int x) { int tmp=0; while(x) tmp+=(x&1),x>>=1; nowk[cnt]=tmp; } void pre() { FAKE=(1<<n)-1;//可能的状态总数(最大值) for(int i=0;i<FAKE;i++) if(!(i&(i>>1))) bin[++cnt]=i,getnowk(i); }
预处理状态部分
现在我们来说状态!这种棋盘型的状压dp,我们一般把“行”作为状态。
设f[i][j][k]为在前i行,状态序号为j,目前已经放了k个国王的方案。
转移:f[i[[j][k]=sigma( f[i-1][x][k-nowk[j] ) 其中x枚举的是上一行的状态,nowk记录着此状态中放了多少国王,这里减的是当前行状态的国王数。
另外,除了同行互不侵犯,邻行是否互不侵犯我们如何处理?
无敌位运算!
if(bin[j]&bin[k]) continue; //上下可侵犯 if((bin[j]<<1)&bin[k]) continue; // 左上右下侵犯 if(bin[j]&(bin[k]<<1)) continue; // 右上坐下侵犯
于是,这就是状压dp。它把复杂的状态转化为一个数,用位运算处理状态!
code
#include<cstdio> #include<algorithm> //写题解的时候可以输出一下中间结果! using namespace std; typedef long long ll; int n,suming,cnt,FAKE; int bin[3000],nowk[3000];//bin[]: 01串表示的十进制 //nowk[]: 此时放的国王数量 即01串里有多少个1 ll f[15][3000][150]; ll ans; void getnowk(int x) { int tmp=0; while(x) tmp+=(x&1),x>>=1; nowk[cnt]=tmp; } void pre() { FAKE=(1<<n)-1;//可能的状态总数(最大值) for(int i=0;i<FAKE;i++) if(!(i&(i>>1))) bin[++cnt]=i,getnowk(i); } int main() { scanf("%d%d",&n,&suming); pre();//预处理出的状态 cnt是状态总数 //所有的状态:相邻无1的01串 for(int i=1;i<=cnt;i++) f[1][i][nowk[i]]=1; //赋初值,第1行第i个状态一定只有1种方案。 /* for(int i=1;i<=cnt;i++) printf("%d ",bin[i]); printf("\n"); for(int i=1;i<=cnt;i++) printf("%d ",nowk[i]);*/ for(int i=2;i<=n;i++) //第1行已处理出,从第2行开始 for(int j=1;j<=cnt;j++)//枚举当前行(第i行)的状态 for(int k=1;k<=cnt;k++)//枚举上一行(第k行)的状态 { if(bin[j]&bin[k]) continue; if((bin[j]<<1)&bin[k]) continue; if(bin[j]&(bin[k]<<1)) continue; for(int s=suming;s>=nowk[j];s--) f[i][j][s]+=f[i-1][k][s-nowk[j]]; } for(int i=1;i<=cnt;i++) ans+=f[n][i][suming]; printf("%lld",ans); return 0; }
感谢 @KesdiaelKen @p_b_p_b 提供的思路!
*Update
用状压dp能否解决八皇后问题? 答案是否定的,在八皇后问题中要满足行、列、对角线上除了本身没有另外的皇后,因为行、列、对角线是棋盘里整条贯穿边界的线,所以用位运算压缩状态就不太行。
所以状压dp比较适合上下左右仅相邻的情况 ,这种整行的维护就比较困难。
另如https://www.luogu.org/problemnew/show/P2051 ,若把n或m中的一个范围改为10,能不能用状压dp呢?答案依然是否定的,不行的原因与八皇后问题相似。
独立意志与自由思想是必须争的,且须以生死力争。