状压dp 学习笔记
"此刻发生的所有事,都是你过去选择的结果。"
最近打模拟赛在状压dp上总是没有一点思路。来重学一遍。
状态压缩:通过一串 01 码来清晰地表示一个集合的状态。同时,在确定了最低位的前提下,一串 01 码与一个二进制数一一对应。
其本质上是进行了两次操作:
- 给这个集合的每个状态一个编号。
- 通过这个编号,轻易地访问该状态。
状压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 时所需要的最小步数。
#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。但这种做法不乏为一种很好的思路。所以还是放这了。
2.P1896 [SCOI2005] 互不侵犯
很好的一道状压dp入门题。
设状态 f[i][j][s] 定义为所有只摆在前 i 行,已经摆了 j 个国王,并且第 i 行摆放的状态是 s 的所有方案的集合。
限制:
1.第 i-1 行内部不能有两个 1 相邻。
2.第 i-1 行和第 i 行之间也不能相互攻击到。
再给出两个定义:
已经摆完前 i 行,且第 i 行的状态为 a,第 i-1 行的状态为 b,已经摆了 j 个国王的所有方案。
已经摆完前 i-1 行,并且第 i-1 排的状态是 b ,已经摆了 j-count(a) 个国王的所有方案。
其中 count 计算的是这一行有多少个1。
接下来贴代码。
//学习状压dp! #include<bits/stdc++.h> using namespace std; #define int long long const int N=12,M=1<<10,K=110; int n,m; vector<int>state,head[M]; int id[M],cnt[M]; int f[N][K][M]; //判断同一行是否存在相邻的1 bool check(int state){ for(int i=0;i<n;i++){ if((state>>i&1)&&(state>>i+1)&1) return 0; } return 1; } //计算这个状态有多少个1 int count(int state){ int res=0; for(int i=0;i<n;i++) res+=state>>i&1; return res; } signed main(){ cin>>n>>m; for(int i=0;i<(1<<n);i++){ if(check(i)) state.push_back(i); id[i]=state.size()-1; cnt[i]=count(i); } for(int i=0;i<state.size();i++){ for(int j=0;j<state.size();j++){ int a=state[i],b=state[j]; if((a&b)==0&&check(a|b)) head[i].push_back(j); } } f[0][0][0]=1; for(int i=1;i<=n+1;i++) for(int j=0;j<=m;j++) for(int a=0;a<state.size();a++) for(int b:head[a]){ int c=cnt[state[a]]; if(j>=c) f[i][j][a]+=f[i-1][j-c][b]; } cout<<f[n+1][m][0]<<endl; return 0; }
3.P1879 [USACO06NOV] Corn Fields G
与上题比较类似。只是因为不需要记录个数,所以可以删去一维。
#include<bits/stdc++.h> using namespace std; #define int long long const int N=14,M=1<<12,mod=1e8; int n,m,g[1<<N]; //g表示第i行是否可用。 vector<int>state,head[M]; int f[N][1<<N]; bool check(int state){ for(int i=0;i<m;i++) if((state>>i&1)&&(state>>i+1&1)) return 0; return 1; } signed main(){ cin>>n>>m; for(int i=1;i<=n;i++) for(int j=0;j<m;j++){ int t; cin>>t; g[i]+=!t*(1<<j); } for(int i=0;i<1<<m;i++) if(check(i)) state.push_back(i); for(int i=0;i<state.size();i++) { for(int j=0;j<state.size();j++) { int a=state[i],b=state[j]; if(!(a&b)) head[i].push_back(j); } } f[0][0]=1; for (int i = 1;i <= n + 1;i++) { for (int a = 0;a < state.size ();a++) { if (!(state[a] & g[i])) { for (int b : head[a]) f[i][a] = (f[i][a] + f[i - 1][b]) % mod; } } } cout<<f[n+1][0]; return 0; }
4.P2704 [NOI2001] 炮兵阵地
这题不同于上面题目的点在于,状态会受到上面两行的影响。
#include<bits/stdc++.h> using namespace std; const int N = 110, M = 1 << 10; int n, m; int g[N], cnt[M]; int f[2][M][M]; vector<int> state; vector<int> head[M]; bool check(int st) { return !(st & st >> 1 || st & st >> 2); } int count(int st) { int res = 0; while (st) res += st & 1, st >>= 1; return res; } int main() { cin >> n >> m; for (int i = 1, j = 0; i <= n; ++ i, j = 0) for (char c; j < m && cin >> c; ++ j) g[i] += (c == 'H') << j; for (int st = 0; st < 1 << m; ++ st) if (check(st)) state.push_back(st), cnt[st] = count(st); for (int cur_st: state) for (int pre_st: state) if (!(cur_st & pre_st)) head[cur_st].push_back(pre_st); for (int i = 1; i <= n; ++ i) for (int st: state) if (!(g[i] & st)) for (int p1: head[st]) for (int p2: head[p1]) if (!(st & p2)) f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]); int res = 0; for (int st: state) for (int pre: head[st]) res = max(res, f[n&1][st][pre]); cout << res << endl; return 0; }
本题可以滚动数组优化。
for (int i = 1; i <= n + 2; ++ i) for (int st: state) if (!(g[i] & st)) for (int p1: head[st]) for (int p2: head[p1]) if (!(st & p2)) f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]); cout << f[n+2&1][0][0] << endl;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统