状态压缩学习笔记
1.关于二进制
二进制只由 0 和 1 组成,所以用二进制可以表示所有整数。
同时,由于二进制的特殊性,我们可以把它看做一个全集的子集,用每一位上的数字来决定在全集中处于同一位的元素是否在这个子集中。
这里有基本的位运算操作:
| 操作 | 代码 | 意义 |
|---|---|---|
| 取第 |
(S >> k) & 1 |
判断元素 |
| 加入第 |
S \| (1 << k) |
将元素 |
| 删除第 |
S & ~(1 << k) |
将元素 |
| 切换第 |
S ^ (1 << k) |
切换元素 |
| 取 |
S & (-S) |
lowbit |
其中
不难发现,当一个数转为二进制时,它最低位的 1,也就是 lowbit,决定了它最多能被 2 的多少次方整除。设这个数为
这个证明其实很简单,只要根据二进制的定义推就可得证。
同时,二进制还有另外一个性质:设一个数
由第一个性质可以知道二进制可以用于枚举方案数。所以对于某些存在性问题,可以选用二进制来进行更好看便捷的枚举。
2.状态压缩动态规划
这东西其实和爆搜差不多,只不过更加美观了。
但是我还是不会做
大体思路就是枚举所有的方案数,判断当前枚举的方案数是否符合条件,若符合就对其做 DP。DP 的状态转移方程还是因题而异的。
这种方法只有在
例题 1:[COCI 2015/2016 #2] GEPPETTO
这道题就是典型的状压 DP,
对于每个数的二进制,把 0 看做不放第
AC Code:
#include<bits/stdc++.h>
using namespace std;
int n,m,ans=0;
pair<int,int> a[41];
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i) scanf("%d%d",&a[i].first,&a[i].second);
for(int i=0;i<(1<<n);++i){
bool flag=0;
for(int j=1;j<=m;++j){
if(i&(1<<(a[j].first-1))&&i&(1<<(a[j].second-1))){
flag=1;
break;
}
}
if(!flag) ans++;
}
cout<<ans;
return 0;
}
例题 2:[蓝桥杯 2019 省 A] 糖果
对于每个数的二进制,把 0 看成第
AC Code:
#include<bits/stdc++.h>
using namespace std;
int n,m,k,f[1<<20],c[1<<20];
int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;++i){
for(int j=1;j<=k;++j){
int x;
scanf("%d",&x);
if(c[i]&(1<<(x-1))) continue;
else c[i]+=(1<<(x-1));
}
}
memset(f,0x3f3f3f3f,sizeof(f));
f[0]=0;
for(int i=0;i<(1<<m);++i){
for(int j=1;j<=n;++j) f[i|c[j]]=min(f[i|c[j]],f[i]+1);
}
if(f[(1<<m)-1]==0x3f3f3f3f) cout<<-1;
else cout<<f[(1<<m)-1];
return 0;
}
例题 3:[USACO06NOV] Corn Fields G
这道题要考虑的因素很多。可以先预处理出田状态的二进制,0 表示不能种,1 表示能种。然后对于所有状态,0 表示没有草,1 表示有草,进行枚举。去掉非法的情况(即 1 的左右两边存在 1 或者有 1 的地方不能种草),从第二行开始,对第
AC Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e8;
ll m,n,a[13][13],c[13],h[1<<12],f[13][1<<12];
int main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j){
scanf("%lld",&a[i][j]);
c[i]=(c[i]<<1)+a[i][j]; //预处理 c[i]
}
for(int i=0;i<(1<<m);++i){
if(!(i&(i>>1))&&!(i&(i<<1))){ //原理:左右移如果有都是 1 的情况就是非法的
h[i]=1; //方案合法
if((i&c[1])==i) f[1][i]=1; //初始化
}
}
for(int i=2;i<=n;++i){
for(int j=0;j<(1<<m);++j){
if((j&c[i-1])==j&&h[j]){
for(int k=0;k<(1<<m);++k){
if((k&c[i])==k&&!(j&k)&&h[k]){ //如果 j 和 k 有相同的数 !(j&k) 就为假
f[i][k]=(f[i][k]+f[i-1][j])%mod;
}
}
}
}
}
ll ans=0;
for(int i=0;i<(1<<m);++i) ans=(ans+f[n][i])%mod; //对 f[n][i] 进行求和
cout<<ans;
return 0;
}
额外习题(?):
[USACO12MAR] Cows in a Skyscraper G
[USACO13NOV] No Change G
[USACO14FEB] Cow Decathlon G
对于额外习题的第一题,我的方法高达 for(int j=i;j;j=(j-1)&i)。这道题有正解,不要根据超时的思路走 qwq。