状压DP学习总结(一)

目的:减小内存消耗,利用状压dp,所有的状态都可以用一个32位甚至16位的整数,位运算能提高运算的速度。

需要用到一些位运算:&(按位与):均1则1,有0则0。用途:清零,取指定位,保留一位。|:(按位或)均0则0,有1则1。

^:(按位异或)相同为0,相反为1。~:取反操作。<<:左移相当于乘2.但是要注意有符号数和无符号数。>>:右移相当于除2.

数据范围在20以内,就尝试用状态dp解决。分为自定向下的记忆化搜索和自下而上的递推式求解。

去掉最后一位   x >> 1

在最后加一个0   x << 1

在最后加一个1 (x<< 1)+1

把最后一位变成1 x | 1 (|定值为1)

把最后一位变成0   (x | 1) - 1

最后一位取反 x ^ 1

把右数第k位变成1 x | (1 << (k-1))

把右数第k位变成0 x & (~ (1 << (k-1)))

右数第k位取反   x xor (1 shl (k-1))

取末k位 x & ((1<

取右数第k位 (x >> (k-1)) & 1

把末k位变成1 x | ((1 << k)-1) 末k位取反   x ^ ((1 << k)-1)

把右边连续的1变成0 (x & (x+1))

把右起第一个0变成1 x | (x+1)

把右边连续的0变成1 x | (x-1) 取右边连续的1   (x ^ (x+1)) >> 1

           去掉右起第一个1的左边 x&(-x)

问题1:
在n*n(n≤20)的方格棋盘上放置 n 个车(可以攻击所在行、列),求使它们不能互相攻击的方案总数。
分析:
利用组合数学:答案为n!
状态压缩递推:
首先,每一行只能有一个车,只要一行行地放,每行只放一个,保证同一行之间不会有攻击。其次,每一列只能放一个车,只要能记录下哪一列有车,下次不再考虑这一列就可以了。用一个二进制数S来表示某一列是否已经放置了车。例如n=5,S=01101,就表示第一、三、四列(从低位开始)已经放置了车。f[S]表示在状态S下的方案数。
那么状态S是怎么来的呢?
因为是一行行放置的,所以到状态S的时候已经放置到了第三行。
三种情况:
①前两行在第3、 4 列放置了棋子(不考虑顺序,下同),第三行在第 1 列放置;
②前两行在第1、 4 列放置了棋子,第三行在第 3 列放置;
③前两行在第1、 3 列放置了棋子,第三行在第 4 列放置。
如下图所示,依次为以上三种情况,(红,蓝),(红,绿)分别代表两种顺序。
f(01101)=f(00101)+f(01001)+f(01100)

推广状态S,那么f[S] =Σf[S^(1<<(i-1))],其中i是枚举的状态S中每一个1的位置(从低位到高位)。然后依次去掉一个1。

边界条件:f[0] = 1。

 

#include
#include
#include
#include
using namespace std;
const int maxn=(1<<20)-1;
int f[maxn];
int main()
{
  int n;
  cin>>n;
  f[0]=1;
  for(int S=1;S<=(1<0)
      {
       int s=S^1<<(i-1);
       f[S]+=f[s];
      }
    }
  }
  printf("%d",f[(1<

问题二

n在n*n(n≤20)的方格棋盘上放置 n 个车,某些格子不能放,求使它们不能互相攻击的方案总数。

分析:这个题貌似还是容斥原理,但是 不是n!,采用状态压缩,怎么解决呢?枚举当前行的放置位置时可能存在无法放置的格子,判断是否允许放置。对于不能存在的位置用ar表示,不允许放置用0,允许放置用1表示。

#include
#include
#include
#include
using namespace std;
long long f[1<<20];
long long vis[22];
int main()
{
  long long n,m;
  while(cin>>n>>m)
  {
    int a,b;
    memset(f,0,sizeof(f));
    memset(vis,0,sizeof(vis));
    for(int i=0;i>a>>b;
      vis[a]+=1<<(b-1);//某位置不能放置,压缩成整数
    }
    f[0]=1;
    for(int i=1;i< 1<0;j-=(j&-j))
        num++;//计算i这个状态1的个数,也就是最多包含的行数
      for(int j=i;j>0;j-=(j&-j))
      {
        if(!(vis[num]&(j&-j)))
           f[i]+=f[i&~(j&-j)];//判断该位置是否可以放置
      }
    }
    cout<

问题三

给出一个 n*m 的棋盘(n 、m≤ 80,n*m ≤ 80),要在棋盘上放 k(k ≤ 20)个棋子, 使得任意两个棋子不相邻。问有多少种方案。

 分析:这道题和前两道题不一样的是两个棋子不能相邻,而且并未说每行只能放一个旗子,继续运用SC方法显然是困难的。2^80次方是无法求出的,所以就要想到状态压缩,把状态压缩在可承受的范围之内,81=9*9,2^9次方是可以的,即如果n,m 都大于等于 9,将不再满足n*m≤80 这一条件。所以,我们有n 或m 小于等于 8,而2^8 是可以承受的。我们假设 m≤n(否则交换,由对称性知结果不变)n  是行数 m  是列数,则每行的状态可以用 m 位的二进制数表示。但是本题和例 1 又有不同:例 1 每行每列都只能放置一个棋子,而本题却只限制每行每列的棋子不相邻。但是,上例中枚举当前行的放置方案的做法依然可行。我们用数组 s[1..num] 保存一行中所有的num 个放置方案,则s 数组可以在预处理过程中用DFS 求出,同时用c[i]保存第i 个状态中 1 的个数以避免重复计算。开始设计状态。如注释一所说,维数需要增加,原因在于并不是每一行只放一个棋子,也不是每一行都要求有棋子,原先的表示方法已经无法完整表达一个状态。我们用 f[i][j][k]表示第 i 行的状态为s[j]且前i 行已经放置了k 个棋子(2) 的方案数。沿用枚举当前行方案的做法,只要当前行的方案和上一行的方案不冲突即可,“微观”地讲,即s[snum[i]]和s[snum[i-1]]没有同为 1 的位,其中snum[x]表示第x 行的状态的编号。然而,虽然我们枚举了第 i 行的放置方案,但却不知道其上一行(i-1)的方案。为了解决这个问题,我们不得不连第i-1 的状态一起枚举,则可以写出递推式: f[i][s[j]][t] += f[i-1][s[k]][t-c[j]];  i,j,k都需要枚举。

#include
#include
#include
#include
#include
using namespace std;
int s[1<<10];//表示所有的num放置方案
int c[1<<10];//保存第i个状态中1的个数
int f[82][1<<9][21];
//f[i][j][k]表示第i行的状态为S[j]且前i行已经放置了k
//个棋子
int n,m,num,flag,val;
void dfs(int ans,int pos,int flag)
{
  if(pos>n)
  {
    s[++num]=ans;
    c[num]=flag;
    return;
  }
  dfs(ans,pos+1,flag);
  dfs(ans+(1<<(pos-1)),pos+2,flag+1);
}
int main()
{
  while(cin>>n>>m>>val)
  {
    if(n>m)swap(n,m);
    num=0;
    dfs(0,1,0);
    memset(f,0,sizeof(f));
    for(int i=1;i<=num;i++)
       f[1][s[i]][c[i]]=1;
    for(int i=2;i<=m;i++)
    {
      for(int j=1;j<=num;j++)
      {
        for(int r=1;r<=num;r++)
        {
          if(!(s[j]&s[r]))
          {
            for(int k=0;k<=val;k++)
            {
              if(k>=c[j])
               f[i][s[j]][k]+=f[i-1][s[r]][k-c[j]];
            }
          }
        }
      }
    }
    long long sum=0;
    for(int i=1;i<=num;i++)
      sum+=f[m][s[i]][val];
    cout<

其实我这些代码还没搞懂啥意思,希望以后可以搞懂,不过讲真好难啊,状压DP感觉难了好多,位运算确实快了很多,但是代码讲真不容易理解,希望以后可以理解吧。

问题5  在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个 格子。 1 <=N <=9, 0 <= K <= N * N

分析:首先想到搜索,但是搜索应该会超时,因为共有81个格子,绝对超时,在此膜dsy大神。

思路:首先想到贪心,很明显贪心会超时,基础的动规h,一个格子一个格子的扩展,不满足无后效性,那么换一种思路,一列一列的扩展,前一行的状态影响 后一行的状态。如何表示每一行状态,每一行的格子数目<=9,每一个格子有两种状态,放或者不放,放为1,不放时记为0,2^9-1为出现的最大情况(但是是很明显不会出现这种情况),但是每一行不能有相邻的两个1,怎么判断x&(x<<1)==0.注意相关运算&,均1为1,有0则0,|均0才0,其余为1.(我一直记混这个操作),如果已知上一行的状态是x,当前状态是y,xy满足什么条件是合法的?(x&y==0)(x&(y<<1)==0)(x&(y>>1)==0)这三种情况。但是我依然写不出来代码,哭了哭了,别人的代码我又看不懂。好蠢好蠢。

#include
#include
#include
#include
using namespace std;
int c[1<<10],num;//c表示状态对应的1的个数
int s[1<<10];//每种状态
int f[11][1<<10][30];
//f[i][j][k]表示第i行的状态为k且已经放了j个国王的方案数
int n,m,val;
void dfs(int ans,int pos,int flag)//当前状态 位置  1的个数
{
  if(pos>n)//行已经走完了,即每一行都有对应的方案
  {
  //  num++;
    s[++num]=ans;
    c[num]=flag;
    return;//返回上一个位置执行下一种方案
  }
  dfs(ans,pos+1,flag);//当前位置不放,但是这一行不放,
  //就要到下一行了,行数需要加1
  dfs(ans+(1<<(pos-1)),pos+2,flag+1);//当前位置放,1的个数加1
  //放了之后下一个位置就不能放了
}
int main()
{
  while(cin>>n>>m>>val)
  {
    num=0;
    dfs(0,1,0);//全不放,从第一行开始,1的个数是0
    memset(f,0,sizeof(f));
    for(int i=1;i<=num;i++)
    {
      f[1][s[i]][c[i]]=1;
    }//第一行使用状态是s[i],放了c[i]个棋子。
      for(int i=2;i<=n;i++){//前2-n行
        for(int j=1;j<=num;j++)//当前行
        {
          for(int r=1;r<=num;r++)//上一行
          {
            if((s[j]&s[r])||((s[j]>>1)&s[r])||((s[j]<<1)&s[r]))
                         continue;
            for(int k=0;k<=val;k++)//前k行放了几个棋子
            {
              if(k>=c[j])//到当前行为止放的棋子总数
              //要不小于选择当前状态放会添加的棋子数
                f[i][s[j]][k]+=f[i-1][s[r]][k-c[j]];
            }
          }
        }
      }
    long long sum=0;
    for(int i=1;i<=num;i++)
      sum+=f[n][s[i]][val]; //前n行放k个棋子,
      //第n行选择状态s[i]的方案数相加
    cout<

未完,待续................................,不过讲真,状压dp好难的。

推荐练习:poj 1185      hdu1074    poj 3254   poj3311   hdu3001   poj 2288   zoj  4257   poj 2411   hdu 3681.

你可能感兴趣的:(状压dp)