当前位置:   article > 正文

一文带你入门并吃透状态压缩DP

状态压缩dp

【本文比较适合有一定动态规划和位运算基础的童鞋阅读】

首先先讲讲什么是状态压缩

状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1

我们都知道二进制可以用来枚举子集,例如某个问题有8种情况,那么我们可以一个循环,从0到2^3-1,将所有情况枚举出来,这里拓展一个位运算的技巧(i>>j&1): 用来求十进制下的数i第j位是否为1,我们规定如果当前位为1就说明这一位应当被选中

动态规划的问题

状态压缩DP常见问题大概可以分为两类

1.棋盘式(基于连通性)DP

2.集合式DP

个人总结的状态压缩dp三部曲:

  1. 考虑如何状态压缩

  2. 确定状态表示和状态转移方
  3. 根据实际问题确定筛选条件 最关键一步!!!合理情况的筛选一定要考虑完全

解释一下第三步的原因,我们状态压缩是对所有情况进行枚举,而实际的题目中会有限制,我们根据题目要求,先对所有情况进行预处理,用一个bool数组,将不合法的数值标记为false即可

例题引入: P1879 [USACO06NOV]Corn Fields G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意:每一个是1的都可以种植草地,每个草地的上下左右不能有草地,求方案数

第一步:分析如何状态压缩

规定1表示种植草地,0表示不种值,所以每一行的状态由[0,2^m-1]表示,m表示列数 

第二步:状态表示和状态转移方程

初状态 f[0][0] = 1(默认一行也不摆也是一种方案)

f[i][j]表示摆完了前i行,第i行的状态是j的方案数

第i行是在前i-1的基础上加上的所以状态转移方程很容易得出:f[i][j] += f[i-1][k]; 

第三步: 根据实际情况筛选合理情况

本题中要求若中间确定种植草地,则上下左右不允许出现草地,即不允许出现1

我们可以通过位运算来确定是否合法

1.行内合法 位运算技巧: !(i & i >> 1)为真,则为合法情况

技巧讲解: i>>1后i-1位来到了i位,i位来到了i+1位的情况,如果i&i>>1等于0不就是说明i-1和i,i和i+1都是不相等的状态吗,也就是我们当前第i位是1,那么i-1位和i+1位都是0,就保证了左右没有草地

2.行间是否合法 

我们假设第i-1行的状态是a,第i行的状态是b,那么需要满足的条件是!(a&b),就是两行间不能有相邻的草地,

3.枚举的状态数是否符合实际的条件

本题的实际条件就是不能超过本行的草地数量

例如一个3行5列的土地,当前行只有两列是可以种植草地的,所以当前土地的草地数为g[i],那么应该满足a&g[i] = a,就是a应该是g[i]的一个子集,相当于a∩g[i] = a

  1. #include<iostream>
  2. using namespace std;
  3. const int mod = 1e9;
  4. int g[14];
  5. int s[1<<14];//s数组用来存储状态,所以列数[1,12],所以可以开到2^14
  6. int f[14][1<<14];f数组是动态规划的数组,第一维表示行数,第二维表示当前状态
  7. int main()
  8. {
  9. int n,m;
  10. cin>>n>>m;
  11. for(int i = 1;i <= n;i++)
  12. {
  13. for(int j = 1;j <= m;j++)
  14. {
  15. int x;
  16. cin>>x;
  17. g[i] = (g[i] << 1) + x;//位运算
  18. }
  19. }
  20. int cnt = 0;
  21. for(int i = 0;i < (1<<m);i++)
  22. {
  23. if(!(i&i>>1)) s[cnt++] = i;//预处理,将符合条件的状态存下来
  24. }
  25. f[0][0] = 1;//初始
  26. for(int i = 1;i <= n;i++)
  27. {
  28. for(int a = 0;a < cnt;a++)
  29. {
  30. for(int b = 0;b < cnt;b++)
  31. {
  32. if((g[i]&s[a]) == s[a] && !(s[a] &s[b]))//过滤行间和行内要满足的条件
  33. f[i][a] = (f[i-1][b] + f[i][a]) % mod; //状态转移
  34. }
  35. }
  36. }
  37. long long ans ;
  38. for(int i = 0;i < cnt;i++) //遍历所有状态,将第n行的合理状态加起来
  39. {
  40. ans =(ans + f[n][i]) % mod;
  41. }
  42. cout<<ans;
  43. }

最后遍历所有的合理状态也可以根据状态转移方程,第n+1的方案是由第n行转移过来的,

所以f[n+1][0] = (f[n][a1] + f[n][a2] + .....f[n][acnt]),也就是相当于遍历了一遍,所以我们也可以这样写

  1. for(int i = 1;i <= n+1;i++)//循环到第n+1
  2. {
  3. for(int a = 0;a < cnt;a++)
  4. {
  5. for(int b = 0;b < cnt;b++)
  6. {
  7. if((g[i]&s[a]) == s[a] && !(s[a] &s[b]))
  8. f[i][a] = (f[i-1][b] + f[i][a]) % mod;
  9. }
  10. }
  11. }
  12. cout<<f[n+1][0];

例题引入 :U204941 蒙德里安的梦想 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

第一步 首先先给出一个明显的结论:横放格子的方案数等于总方案数

当竖放的格子确定好位置后,那么横放的格子也就确定了,因为摆放的方式只有横放或者竖放,当竖放确定后,剩余的坑位也就确定了形状,显然这题就是讨论哪个格子是怎么放置的,我们看它给出的数据范围是[1,11],我们可以考虑按列摆放,那么我们枚举二进制,根据题目实际分析应该是[0,2^m-1],行数是n,那么就有n位。

我们可以规定若某行是1,表示横,某行为0,表示竖放。

第二步

 用f[i][j]记录第i列第j个状态。j状态位等于1表示上一列有横放格子,本列有格子捅出来。转移方程很简单,本列的每一个状态都由上列所有“合法”状态转移过来f[i][j] += f[i - 1][k]

初始化条件:f[0][0] = 1第0列只能是状态0,无任何格子捅出来。

终止状态:f[m+1][0]。第m + 1列不能有东西捅出来。

第三步

分析实际情况:

那么在这个题中我们应该可以想到第i列和第i-1不能为同时为1,因为我们规定有1表示当前的列是横放格子的第二个格子,如果i列为1就说明i-1列应该是竖放格子的第一个

所以条件为!(j&k)为真

第二个条件:设当前列的状态是state,,上一列状态为last,我们知道是把行作为状态数,说明这一行有格子,那么我们没有格子的地方最后是要填满竖放格子的,竖放格子的高为2,也就是我们两个放格子的行之间的行数应该是偶数个,如果是奇数个不能放下竖放格子,肯定会有空格,所以state|last 应该是不能出现连续奇数个0,这个条件我们可以通过预处理,将满足条件的状态存下来,下面通过图来解释

绿色为上一列的格子,蓝色表示这一列的格子,last |state 就是两个状态只要出现1,最后的结果的这一行就为1,因为两个状态的交集在第i列,无论是i-1列的状态出现1还是i列出现1,都有第i列肯定有格子,只有两个状态的当前行都为0,第i列的格子才没有被填,所以我们要找的就是 state|last中是否有连续的0

 

  1. #include<iostream>
  2. #include<cstring>
  3. using namespace std;
  4. const int N = 13,M = 1<<N;
  5. long long f[N][M];
  6. int cnt;
  7. bool st[M];
  8. int main()
  9. {
  10. int n,m;
  11. while(cin>>n>>m,n||m)
  12. {
  13. memset(f,0,sizeof f);
  14. for(int i = 0;i < (1<<n);i++)
  15. {
  16. cnt = 0;
  17. st[i] = true;
  18. for(int j = 0;j < n;j++)
  19. {
  20. if(i >> j & 1)
  21. {
  22. if(cnt & 1)
  23. {
  24. st[i] = false;
  25. cnt = 0;
  26. }
  27. }
  28. else cnt++;
  29. }
  30. if(cnt & 1) st[i] = false;
  31. }
  32. f[1][0] = 1;//初始条件 第一列不能作为横放格子的第二个格子,只有0一种情况
  33. for(int i = 2;i <= m+1;i++)
  34. {
  35. for(int j = 0;j < (1<<n);j++)
  36. {
  37. for(int k = 0;k <(1<<n);k++)
  38. {
  39. if(!(j & k)&& st[j|k]) f[i][j] += f[i-1][k];
  40. }
  41. }
  42. }
  43. cout<<f[m+1][0]<<endl;//第m+1行应该是没有格子的情况
  44. }
  45. }

例题引入:P1896 [SCOI2005] 互不侵犯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意很好理解,就是说如果当前点放了国王,以它为中心的九宫格都不能放置国王

二:状态表示:f[i][j][a]:表示前i行放了j个国王,第i行状态为a的方案数

状态转移:用c数组存储当前行的为a状态时的国王数

f[i][j][a] += f[i-1][j-c[k][b];

三.筛选条件

 1.行内合法:与它左右的方格不能同时放1,!(i&i>>1)为真

2.行间合法:和它的上,左上,右上,下都不能同时放1,!(a&b),!(a&b>>1),!(a&b<<1)都为真时,才可以兼容

  1. #include<iostream>
  2. using namespace std;
  3. long long f[14][144][1<<12];
  4. int nums[1<<12];
  5. int s[1<<12];
  6. int n,cnt;
  7. void init()
  8. {
  9. for(int i = 0;i < (1<<n);i++)
  10. {
  11. if(i&(i>>1)) continue;
  12. s[cnt++] = i;
  13. for(int j = 0;j < n;j++)
  14. {
  15. nums[i] += (i>>j&1);//存储每个状态表示里行间合格的国王数
  16. }
  17. }
  18. }
  19. int main()
  20. {
  21. int k;
  22. cin>>n>>k;
  23. init();//预处理行间
  24. f[0][0][0]=1;
  25. for(int i = 1;i <= n+1;i++)//小技巧:遍历到n+1,那么i+1行的方案数都由i行转移,所有方案都存储在i+1行中
  26. {
  27. for(int j = 0;j <= k;j++)
  28. {
  29. for(int a = 0;a < cnt;a++)
  30. {
  31. for(int b = 0;b < cnt;b++)
  32. {
  33. int c = nums[s[b]];
  34. if(j - c >= 0 && !(s[a] & s[b]) && !(s[a] & (s[b]>>1)) && !(s[a]&(s[b]<<1)))
  35. {
  36. f[i][a][j] += f[i-1][b][j-c];
  37. }
  38. }
  39. }
  40. }
  41. }
  42. // cout<<cnt<<endl;
  43. cout<<f[n+1][0][k];
  44. }

例题引入 P2704 [NOI2001] 炮兵阵地 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这道题和第一题十分相似,但是要求是上下左右各多了一格。

状态表示 f[i][a][b] :第i行的状态为a,i-1行状态为b。

状态转移 f[i][a][b] 是由上一行f[i-1][b][c]转移而来。

筛选条件

1.行内:!(i&i>>2)和!(i&i>>1)同时为真

2.行间  a,b,c相与必须为0

3.不能在山地上布署

  1. #include<iostream>
  2. using namespace std;
  3. int g[110];
  4. int cnt;
  5. int num[1<<10];
  6. int s[1<<10];
  7. int f[2][1<<10][1<<10];
  8. int main()
  9. {
  10. int n,m;
  11. cin>>n>>m;
  12. for(int i = 1;i <= n;i++)
  13. {
  14. for(int j = 1;j <= m;j++)
  15. {
  16. char ch;
  17. cin>>ch;
  18. int x = 0;
  19. if(ch == 'P') x = 1;
  20. g[i] = (g[i]<<1) + x;
  21. }
  22. }
  23. for(int i = 0;i < (1<<m);i++)
  24. {
  25. if(!(i & i >>1)&&!(i & i >>2))
  26. {
  27. s[cnt++] = i;
  28. for(int j = 0;j < m;j++)
  29. {
  30. num[i] += (i >> j & 1);
  31. }
  32. }
  33. }
  34. // cout<<cnt<<endl;
  35. //f[0][0][0] = 1;
  36. for(int i = 1;i <= n +2;i++)
  37. {
  38. for(int a = 0;a < cnt;a++)
  39. {
  40. for(int b = 0;b < cnt;b++)
  41. {
  42. for(int c = 0;c < cnt;c++)
  43. {
  44. if(!(s[a]&s[b]) && !(s[a]&s[c]) && !(s[b]&s[c]) && (g[i-1]&s[b])==s[b] &&(g[i]&s[a])==s[a])
  45. {
  46. f[i&1][a][b] = max(f[i&1][a][b],f[i-1&1][b][c]+num[s[a]]);
  47. }
  48. }
  49. }
  50. }
  51. }
  52. cout<<f[n+2&1][0][0];
  53. return 0;
  54. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/article/detail/50879?site
推荐阅读
相关标签
  

闽ICP备14008679号