"此刻发生的所有事,都是你过去选择的结果。"
最近打模拟赛在状压dp上总是没有一点思路。来重学一遍。
状态压缩:通过一串 0 1 码来清晰地表示一个集合的状态。同时,在确定了最低位的前提下,一串 0 1 码与一个二进制数一一对应。
其本质上是进行了两次操作:
- 给这个集合的每个状态一个编号。
- 通过这个编号,轻易地访问该状态。
状压dp常见题型:
- 棋盘式,求方案数。
- 集合,求最短路径。
首先,我们需要掌握一些位运算的小技巧:
1.判断一个数字x二进制下第i位是不是等于1。(最低第 1 位)
方法:if(((1<<(i−1))&x)>0)
将1左移 i -1 位,相当于制造了一个只有第 i 位上是 1,其他位上都是 0 的二进制数。然后与 x 做与运算,如果结果 > 0, 说明 x 第 i 位上是 1,反之则是 0。
2.将一个数字x二进制下第 i 位更改成 1。
方法:x=x|(1<<(i−1))
证明方法与 1 类似。
3.将一个数字x二进制下第 i 位更改成 0。
方法:x=x&~(1<<(i−1))
4.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x−1)
下面是例子。
1.P2622 关灯问题
题意略。
可以发现n的范围<=10。此时想到状压dp的时间复杂度为指数级别,刚好符合本题要求。
考虑从0开始不断向上枚举,遍历 m 个控制效果。通过某个控制效果可以达到状态 t ,那么设 fs 表示到达状态 s 时所需要的最小步数。
得到:
ft=minzj(i)=t { fi }+1
#include<bits/stdc++.h> using namespace std; const int inf=0x3f3f3f3f; const int maxn=12,maxm=1025; int n,m,t,f[1<<maxn],a[maxm][maxn]; int main(){ cin>>n>>m; for(int i=1;i<=m;i++) for(int j=1;j<=n;j++) cin>>a[i][j]; fill(f+1,f+maxm+1,inf); for(int i=0;i<(1<<n);i++) for(int k=1;k<=m;k++){ t=i; for(int j=0;j<n;j++) if((a[k][j+1]==1&&!(i&1<<j))||(a[k][j+1]==-1&&(i&1<<j))) t^=(1<<j); f[t]=min(f[t],f[i]+1); } if(f[(1<<n)-1]==inf) cout<<"-1"<<endl; else cout<<f[(1<<n)-1]<<endl; return 0; }
#注意,其实本题的正解应当为记忆化搜索或者状压最短路。状态是有后效性的。状压dp的做法在洛谷上已经被hack。但这种做法不乏为一种很好的思路。所以还是放这了。