zoukankan      html  css  js  c++  java
  • 状压入门--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("
    ");
        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呢?答案依然是否定的,不行的原因与八皇后问题相似。

  • 相关阅读:
    数据结构 -- 栈(一)
    数据结构 -- 栈(二)
    Linux 静态库 & 动态库
    Python及Pycharm安装详细教程
    Makefile研究(三) —— 实际应用
    Makefile研究(二)—— 完整可移植性模板
    Makefile研究 (一)—— 必备语法
    JSON 下 -- jansson 示例
    C语言中的static 详细分析
    Linux 命令 -- tar
  • 原文地址:https://www.cnblogs.com/nopartyfoucaodong/p/9356506.html
Copyright © 2011-2022 走看看