导语
状压DP分两大类,一类是集合式,另一类就是棋盘式(即基于连通性)。
其中,集合式状压DP难度相较后者略大,形式复杂多变。而棋盘式状压DP的题目样式都相差不多,解法也都殊途同归,因此将这一类题进行总结,不难归结出一套解题模板。
正文
我们先看以下三个例题。
国王
算法概述
状态表示:f[i,j,s]表示已经在前i行放了j个国王,且第i行的状态为s的方案数。
状态转移:考虑第i-1行所有合法状态x,则f[i,j,s]=∑f[i-1,j-count(s),x],其中count(s)表示s中国王的个数。
算法流程:
1.考虑将一行的状态进行压缩,然后预处理所有合法状态。
2.枚举所有合法状态,对于每一种合法状态,处理该状态可由哪些状态转移过来。
3.循环枚举DP状态,进行状态转移。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=12; int stt[1<<N]; int cnt[1<<N]; int pre[1<<N][1<<N]; int sum[1<<N]; ll f[N][N*N][1<<N]; int n,m,cnt_stt; bool check(int state) { for(int i=0;i<n-1;i++) if(state>>i&1 && state>>i+1&1)return false; return true; } int count(int state) { int ans=0; for(int i=0;i<n;i++) if(state>>i&1)ans++; return ans; } int main() { scanf("%d%d",&n,&m); for(int i=0;i<(1<<n);i++)if(check(i))stt[++cnt_stt]=i,cnt[i]=count(i); for(int i=1;i<=cnt_stt;i++) for(int j=1;j<=cnt_stt;j++) if((stt[i]&stt[j])==0 && check(stt[i]|stt[j]))pre[stt[i]][++sum[stt[i]]]=stt[j]; f[0][0][0]=1; for(int i=1;i<=n+1;i++) for(int j=0;j<=m;j++) for(int s=1;s<=cnt_stt;s++) { int a=stt[s]; if(j<cnt[a])continue; for(int x=1;x<=sum[a];x++) { int b=pre[a][x]; f[i][j][a]+=f[i-1][j-cnt[a]][b]; } } printf("%lld ",f[n+1][m][0]); return 0; }
玉米田
算法概述
状态表示:f[i,a]表示已经种了前i行,且第i行的状态为a的方案数。
状态转移:考虑第i-1行的所有合法状态b,则f[i,a]=∑f[i-1,b]。
算法流程:
1.预处理所有合法状态。
2.预处理对于每一个合法状态,能够转移到其的所有合法状态的集合。
3.循环状态表示,考虑当前行状态是否与地图限制相矛盾,然后循环状态转移。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=14,M=1<<12,mod=1e8; int stt[1<<N]; int cnt[1<<N]; int pre[M][M]; int f[N][1<<N]; int g[N]; int n,m,cnt_stt; bool check(int state) { for(int i=0;i<m-1;i++) if(state>>i&1 && state>>i+1&1)return false; return true; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=0;j<m;j++) { int x;scanf("%d",&x); g[i]+=!x<<j; } for(int i=0;i<(1<<m);i++)if(check(i))stt[++cnt_stt]=i; for(int i=1;i<=cnt_stt;i++) for(int j=1;j<=cnt_stt;j++) if((stt[i]&stt[j])==0)pre[stt[i]][++cnt[stt[i]]]=stt[j]; f[0][0]=1; for(int i=1;i<=n+1;i++) for(int j=1;j<=cnt_stt;j++) { int a=stt[j]; if(g[i]&a)continue; for(int s=1;s<=cnt[a];s++) { int b=pre[a][s]; (f[i][a]+=f[i-1][b])%=mod; } } printf("%d ",f[n+1][0]); return 0; }
炮兵阵地
算法概述
状态表示:f[i,a,b]表示已经放了前i行,且第i-1行状态为a,第i行状态为b的所有方案中的最大值。
状态转移:考虑第i-2行所有合法状态c,则f[i,a,b]=min{f[i-1,c,a]+count(b)},其中count(b)表示状态b中放置的个数。
算法流程:
1.预处理所有合法状态。
2.枚举DP状态,考虑当前状态与地图限制是否矛盾,然后枚举DP转移,考虑状态之间的限制。
考虑到空间限制,需要用滚动数组优化。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=110,M=11; int stt[1<<M]; int cnt[1<<M]; int f[2][1<<M][1<<M]; int g[N]; int n,m,cnt_stt; bool check(int state) { for(int i=0;i<=m-2;i++) if((state>>i&1) && (state>>i+1&1 || state>>i+2&1))return false; return true; } int count(int state) { int ans=0; for(int i=0;i<m;i++)ans+=state>>i&1; return ans; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) { static char ch[M]; scanf("%s",ch); for(int j=0;j<m;j++) if(ch[j]=='H')g[i]+=1<<j; } for(int i=0;i<(1<<m);i++) if(check(i))stt[++cnt_stt]=i,cnt[i]=count(i); for(int i=1;i<=n+2;i++) for(int j=1;j<=cnt_stt;j++) for(int k=1;k<=cnt_stt;k++) { int a=stt[j],b=stt[k]; if(g[i-1]&a|g[i]&b)continue; for(int x=1;x<=cnt_stt;x++) { int c=stt[x]; if((a&b)|(a&c)|(b&c))continue; f[i&1][a][b]=max(f[i&1][a][b],f[i-1&1][c][a]+cnt[b]); } } printf("%d ",f[n+2&1][0][0]); return 0; }
模型总结
常规步骤:
1.预处理所有合法状态。
2.若空间允许,则对于每一个合法状态,预处理能够转移到其的所有合法状态的集合。
3.循环DP状态,考虑地图本身的限制,若无矛盾,则循环DP转移。