状态压缩,是一种利用二进制的暴力枚举法.
介绍一下几个二进制运算符,以后都经常用到
与& 1&0 = 0 1&1 = 1 0&0 = 0
或| 1|0 = 1 1|1=1 0|0=0
异或^ 1^0=1 0^0 =0 1^1 = 0
取反~ ~1=0 ~0=1
& 是两边都是1结果才为1
| 只要有一个是1就是1
^ 只有两边不相等的时候才是1
~ 0变1,1变0
到这里还很简单吧?
然后,现在有一个数为7,二进制位0111
还有一个数11,二进制1011
问你7|11为---------------------------------------------------当然是1111也就是15了!!
看起来你对这几个位运算比较熟悉了,emmmm,那我再介绍几个公式(重点,敲黑板)
以下操作都是对数k而言 | 解释 | |
判断k得第i位是不是1 | k&(1<<(i- 1)) | 1<<(i-1)构造了一个只在第i位为1的数,进行&操作,只有当第i位都是1结果才为1,那么可以利用结果判断. |
将两个二进制的1合并 | k|b | 这个简单,合并1直接用| |
将k的第i位置为1 | k|=(1<<(i-1)) | 因为是|,所以K的1全部保留,而且因为(1<<(i-1))的第i位有1 |
将k的第i位置为0 | k&=~(1<<(i-1)) | 和上面差不多,不过因为取反,所以k与了一个第i位为0其他位都是1 的数 |
相信你有点迷糊了,不过别担心,如果看不懂直接往下看例题,我会解释的。
第一个例题:方格取数
题目大意:给你一个n*n的格子的棋盘,每个格子里面有一个非负数。
从中取出若干个数,使得任意的两个数所在的格子没有公共边,就是说所取的数所在的2个格子不能相邻,并且取出的数的和最大。
思路:既然在说状压,那我就开门见山了。我们可不可以用二进制来快捷得表示某一行取数情况呢?
当然可以!!举个例子,当n等于5时
拿7来说,二进制为00111,说明取了这一行得第3,4,5个数
那么当n=5时,用多少个数可以表示所有状态呢?答案是2^5
我们进一步观察发现,7是不合法得状态,因为数字不能连续取出,也就是二进制中得1不能连续出现
所以,我们先用一个for循环,求出在2^5个状态中合法的状态
for(int i=0;i<(1<<5);i++) { if(i & (i<<1) ) continue;//不合法 if(i & (i>>1) ) continue;//不合法 vector[cnt++]=i;//储存状态 }
那么你肯定要问,为什么i&(i<<1)是不合法的状态的
那我们的老朋友7来说,00111,左移一位也就是01110
相与的结果就是00110,结果不是0,所以不合法。这个仔细想想,很好理解。
那么我们进入第二步,设计状态
定义dp[i][j]为枚举到第i行状态为j的最大收益
那么转移方程dp[ i ] [ j ] =max(dp [ i ] [ j ] ,dp [ i-1 ] [ k ]+当前行状态为j的收益)
其中状态vector[j]不能和vector[k]上下相邻,也就是相同位上不能同时为1
for(int i=1;i<=n;i++)//枚举行数 { for(int j=1;j<cnt;j++)//枚举事先储存在vector中的状态 { int val=ji(i,vector[j]);//计算第i行状态vector[j]的收益 for(int k=1;k<cnt;k++) { if( (vector[j]&vector[k]) ) continue;//不能上下相邻 dp[i][j]=max(dp[i][j],dp[i-1][k]+val); } } }
转移之类的都好说,我们来到了最后一个问题,如何求出第i行状态为vector[j]的收益呢?
比如对于状态vector[j],我们逐位判断是不是1,是就加上。然后把vector[j]右移一位,继续判断
int ji(int i,int k)//第i行状态为k { int ans=0,cnt=n; while(k){ if(k&1) ans+=a[i][cnt];//最后一位是1,加上 k>>=1;cnt--; } return ans; }
那么看到这里,恭喜你初步掌握了这种状态间的压缩。好好再体会一下下面完整的代码
#include <iostream> using namespace std; #define max(a,b) (((a)>(b))?(a):(b)) int a[19][19],dp[19][1<<17],vector[1<<17],n,cnt=1; int ji(int i,int k)//第i行状态为k { int ans=0,cnt=n; while(k){ if(k&1) ans+=a[i][cnt];//最后一位是1,加上 k>>=1;cnt--; } return ans; } int main() { cin>>n; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cin>>a[i][j]; for(int i=0;i<(1<<n);i++) { if(i & (i<<1) ) continue;//不合法 if(i & (i>>1) ) continue;//不合法 vector[cnt++]=i;//储存状态 } int maxn=0; for(int i=1;i<=n;i++)//枚举行数 { for(int j=1;j<cnt;j++)//枚举事先储存在vector中的状态 { int val=ji(i,vector[j]);//计算第i行状态vector[j]的收益 for(int k=1;k<cnt;k++) { if( (vector[j]&vector[k]) ) continue;//不能上下相邻 dp[i][j]=max(dp[i][j],dp[i-1][k]+val); maxn=max(maxn,dp[i][j]); } } } cout<<maxn; return 0; }
这里还有一些状压的入门题,拿去不谢哦(●'◡'●)
蓝桥杯分糖果:http://oj.ecustacm.cn/problem.php?id=1460