状压入门--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;
}
View Code

 

 

感谢 @KesdiaelKen @p_b_p_b 提供的思路!

 

 

 

 

*Update 

用状压dp能否解决八皇后问题? 答案是否定的,在八皇后问题中要满足行、列、对角线上除了本身没有另外的皇后,因为行、列、对角线是棋盘里整条贯穿边界的线,所以用位运算压缩状态就不太行。

所以状压dp比较适合上下左右仅相邻的情况 ,这种整行的维护就比较困难。

另如https://www.luogu.org/problemnew/show/P2051   ,若把n或m中的一个范围改为10,能不能用状压dp呢?答案依然是否定的,不行的原因与八皇后问题相似。

posted @ 2018-07-23 19:50  cellur925&Chemist  阅读(401)  评论(2编辑  收藏  举报