0.前言
位运算记不记得?
(n>>k)&1 取出非负数n的第k位
n&((1<<K)-1) 取出非负数n的后k位
n^(1<<k)把非负数n的第k位取反
n^(1<<k)把非负数的第k位取反

1.定义与例题引入
状压 DP 是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的。 ——oiwiki
那么怎么个状态压缩法呢?
例如这个题: P1896 [SCOI2005] 互不侵犯
在N×N 的棋盘里面放K 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8 个格子。(1≤N≤9,0≤K≤N×N)
这里我们发现好像我们可以设几个状态,分别是放到第几行,放了多少个国王,和如何放置的。前两个都好说,但是第三个却让我们很头疼。如果按照常规思路来想,我们肯定会开一个非常非常大的数组来表示这个放置状态,而且甚至移动方向是8个方向,更头疼,怎么办?
状态压缩就是来干这种事的,我们可以用一个二进制数来表示国王放置的状态。设一个二进制数,当它的一个数位表示1的时候就表明放置的国王,如果表示0就表示没有放置国王,如下图。

这样的话我们只需要一个十进制整数就可以对应唯一一个放置国王的状态,也就不需要开那么多的空间啦。
但是对于一个状态来说,有可能合法,也有可能并不合法,并且在判断的时候我们还要处理出攻击范围以及放置了多少个国王,很麻烦。。。。?
我们可以预处理啊!
1 2 3 4 5 6 7
| cin>>n>>k; int tt=1<<n; for(int i=0;i<tt;i++){ isok[i]=((i&(i<<1))==0)&&((i&(i>>1))==0); attack[i]=i|(i<<1)|(i>>1); cnt1[i]=cnt1[i>>1]+(i&1); }
|
接下来我们来看isok这个数组怎么判断的,还是上面那个图,我们对i & (i<<1)做一下解释。

如果有非法的情况呢?

右移同理。
那么attack攻击范围如何处理呢,如下图:

对于1的统计cnt,这个就不多说了自己画图吧(逃
那么开始dp,其实之前已经说出来转移方程了,这一贴一个oiwiki的

故有代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| f[0][0][0]=1; for(int i=0;i<n;i++){ for(int j=0;j<tt;j++){ for(int l=0;l<=k;l++){ if(f[i][j][l]){ for(int p=0;p<tt;p++){ if(isok[p]&&(attack[p]&j)==0){ f[i+1][p][l+cnt1[p]]+=f[i][j][l]; } } } } } }
|
让后拼装以下,就有了AC代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| #include<iostream> #include<cstring> #define ll long long using namespace std; const int MN=(1<<10)+15; int isok[MN],attack[MN],cnt1[MN],n,k; ll ans,f[121][MN][121]; int main(){ cin>>n>>k; int tt=1<<n; for(int i=0;i<tt;i++){ isok[i]=((i&(i<<1))==0)&&((i&(i>>1))==0); attack[i]=i|(i<<1)|(i>>1); cnt1[i]=cnt1[i>>1]+(i&1); } f[0][0][0]=1; for(int i=0;i<n;i++){ for(int j=0;j<tt;j++){ for(int l=0;l<=k;l++){ if(f[i][j][l]){ for(int p=0;p<tt;p++){ if(isok[p]&&(attack[p]&j)==0){ f[i+1][p][l+cnt1[p]]+=f[i][j][l]; } } } } } } for(int i=0;i<tt;i++){ ans+=f[n][i][k]; } cout<<ans; return 0; }
|
当然最后统计答案的时候需要一个一个枚举状态。
农场主John 新买了一块长方形的新牧场,这块牧场被划分成M 行N 列(1≤M≤12,1≤N≤12),每一格都是一块正方形的土地。John 打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John 不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
John 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
输入样例将给出每一格的贫瘠和肥沃的状态
根据上面的思想,我们可以很容易设计出来状态转移方程。
设f[i][j]表示前i行已经种完,开始种第i行,按照j的状态进行种植能获得的方案
故有f[i][j]=f[i][j]+f[i−1][k],其中k是枚举上一行的的种植状态
我们显然可以知道,对于种植状态,判断条件有2个,第一个是该土地是否肥沃(能否种植),第二个是种植时不能出现相邻,如下,橙色为种植点,红色为不能种植点。
对于该一行的种植很简单,和之前一样照常写isok就可以了,但是竖直方向怎么
处理?很简单我们发现如果有覆盖种植的话那么与起来一定会出现一个大于0的数,没有那么一定为0。

放码过来吧!
注意这里的isok写法不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include<iostream> using namespace std; const int MN=15,MK=1<<MN,MOD=1e8; int n,m,map[MN][MN],st[MK],isok[MK],f[MN][MK],ml; int main(){ cin>>n>>m; ml=(1<<m); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>map[i][j]; } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ st[i]=(st[i]<<1)+map[i][j]; } } for(int i=0;i<ml;i++){ isok[i]=((i&(i<<1))==0); } f[0][0]=1; for(int i=1;i<=n;i++){ for(int j=0;j<ml;j++){ if(isok[j]&&((j&st[i])==j)){ for(int k=0;k<ml;k++){ if(!(j&k)){ f[i][j]=(f[i][j]+f[i-1][k])%MOD; } } } } } int ans=0; for(int i=0;i<ml;i++){ ans=(ans+f[n][i])%MOD; } cout<<ans; return 0; }
|
例题2: P10963 Islands and Bridges
给定一张由岛屿和连接这些岛屿的桥梁构成的地图,众所周知,哈密顿路径是沿着桥梁的路径,能够恰好访问每个岛屿一次。在我们的地图中,每个岛屿还关联一个正整数值。我们定义一种哈密顿路径为 最佳三角形哈密顿路径,其最大化以下描述的值。
假设有n 个岛屿。哈密顿路径C1,C2,…,Cn 的值分为三部分计算。设Vi 为岛屿Ci 的值。第一部分为路径中每个岛屿的Vi 值的总和。第二部分,对于路径中的每条边CiCi+1,加上Vi×Vi+1 的积。第三部分,对于路径中的每三个连续岛屿Ci,Ci+1,Ci+2,如果它们在地图中形成一个三角形(即Ci 和Ci+2 之间有桥),加上Vi×Vi+1×Vi+2 的积。
最佳三角形哈密顿路径很可能(但不一定)包含多个三角形。可能会存在多个最佳三角形哈密顿路径。你的第二个任务是找出这样的路径的数量。
对于三角形路径,在DP中如果考虑枚举端点拼凑,那么时间复杂度是不可承受的,我们可以直接在状态中加上2个点,枚举第3个点即可。
那就设fi,j,S表示枚举2个点i,j,之前加入点的集合为S。用邻接矩阵存图判断起来好一点…
这里邻接矩阵设为mp
转移方程也很简单,枚举3个点,就有:
fi,j,s=mp[i][j]=1,mp[j][k]=1maxfj,k,S⊕2i−1+Vi+Vi×Vj+{Vi×Vj×Vk0mp[i][k]=1mp[i][k]=0
路径数怎么统计,如果用pre数组记录直接爆炸,那么直接考虑dp数量,设gi,j,S,字母含义见上,有:
gi,j,S=⎩⎪⎪⎨⎪⎪⎧gj,k,S⊕2i−1gj,k,S⊕2i−1+gi,j,Sgi,j,Sfi,j,S<fj,k,S⊕2i−1+tfi,j,S=fj,k,S⊕2i−1+tfi,j,S>fj,k,S⊕2i−1+t
故有代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| #include<bits/stdc++.h> #define int long long using namespace std;
const int MN = 14; int T; int f[MN][MN][1 << MN], g[MN][MN][1 << MN], v[MN], n, m; bool mp[MN][MN];
void solve() { memset(v, 0, sizeof(v)); memset(f, 0, sizeof(f)); memset(g, 0, sizeof(g)); memset(mp, 0, sizeof(mp));
cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> v[i]; }
for (int i = 1; i <= m; i++) { int u, v; cin >> u >> v; mp[u][v] = mp[v][u] = 1; }
if (n == 1) { cout << v[1] << " " << 1 << '\n'; return; }
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j || !mp[i][j]) continue; f[i][j][(1 << (i - 1)) | (1 << (j - 1))] = v[i] * v[j] + v[i] + v[j]; g[i][j][(1 << (i - 1)) | (1 << (j - 1))] = 1; } }
for (int i = 0; i < 1 << n; i++) { for (int j = 1; j <= n; j++) { if (!((i >> (j - 1)) & 1)) continue; for (int k = 1; k <= n; k++) { if (j == k || !mp[j][k] || !((i >> (k - 1)) & 1)) continue; for (int p = 1; p <= n; p++) { if (!((i >> (p - 1)) & 1) || j == p || k == p || !mp[k][p] || !g[k][p][i ^ (1 << (j - 1))]) continue; int val = f[k][p][i ^ (1 << (j - 1))] + v[j] + v[j] * v[k] + (mp[j][p]) * v[j] * v[k] * v[p]; if (val == f[j][k][i]) g[j][k][i] += g[k][p][i ^ (1 << (j - 1))]; else if (val > f[j][k][i]) { f[j][k][i] = val; g[j][k][i] = g[k][p][i ^ (1 << (j - 1))]; } } } } }
int ans = 0, cnt = 0; for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (!mp[i][j] || !g[i][j][(1 << n) - 1] || i == j) continue; if (ans < f[i][j][(1 << n) - 1]) { ans = f[i][j][(1 << n) - 1]; cnt = g[i][j][(1 << n) - 1]; } else if (ans == f[i][j][(1 << n) - 1]) { cnt += g[i][j][(1 << n) - 1]; } } }
cout << ans << " " << (cnt >> 1) << '\n'; }
signed main() { cin >> T; while (T--) { solve(); } return 0; }
|
2.状压DP的另一种状压——枚举子集
例题如下:P5911 [POI 2004] PRZ
有n个人需要过桥,第i的人的重量为wi,过桥用时为ti这些人过桥时会分成若干组,只有在某一组的所有人全部过桥后,其余的组才能过桥。桥最大承重为W,问这些人全部过桥的最短时间。
100≤W≤400 ,1≤n≤16,1≤t≤50,10≤w≤100。
数据范围较小,可以进行状压,我们发现这题的关键就是分组别的规划。故我们可以设状态f[i],表示状态为i下过桥最少要用的时间。
状态i表示把每个人选或不选压为二进制。
故有f[i]=min{f[j]+mt[ixorj]}
其中ixorj表示i种除j外剩余的子集,前提j是i的子集
但是我们对于每个状态要统计他们的最大重量和和时间,每个状态都是二进制数,怎么分别取出有没有人在其中呢?
这就是这章要讲的——枚举子集。
怎么枚举?先看代码。
1 2 3 4 5 6 7
| int ml=1<<n; for(int i=0;i<ml;i++){ for(int j=i;;j=(j-1)&i){ } }
|
我怎么证明这个是对的呢?
我们枚举一下,看下图。

详细证明如下:

遍历子集的时间复杂度是O(3n)
故我们可以得到转移代码如下:
1 2 3 4 5 6 7 8
| for(int i=0;i<ml;i++){ for(int j=i;;j=(j-1)&i){ if(mw[i^j]<=w){ f[i]=min(f[i],f[j]+mt[i^j]); } if(!j) break; } }
|
拼装一下,AC代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include<iostream> #include<cstring> using namespace std; const int MN=450,MF=(1<<16)+15; int w,n; int t[MN],wei[MN],ml,f[MF],mt[MF],mw[MF]; int main(){ cin>>w>>n; ml=(1<<n); for(int i=1;i<=n;i++){ cin>>t[i]>>wei[i]; } for(int i=0;i<ml;i++){ for(int j=1;j<=n;j++){ if(i&(1<<(j-1))){ mt[i]=max(mt[i],t[j]); mw[i]+=wei[j]; } } } memset(f,0x3f,sizeof(f)); f[0]=0; for(int i=0;i<ml;i++){ for(int j=i;;j=(j-1)&i){ if(mw[i^j]<=w){ f[i]=min(f[i],f[j]+mt[i^j]); } if(!j) break; } } cout<<f[ml-1]; return 0; }
|
3.状压DP的拓展,三进制优化+DFS(例题引入)
来看题
P7689
给定N×M的矩形,让你在上面放尽量多的2×3的矩形(或3×2,就是横着竖着都可以放),矩形上有一些坏点,放置的矩形不能覆盖这些坏点,求最多能放多少个矩形。
多组测试数据,1≤T≤5,1≤N≤150,1≤M≤10
内存限制:8MB;时间限制:2s
我们不难可以用类似P1879或炮兵阵地类似的思想,也就是平面覆盖问题。但是这里出现一个问题,这里的宽是2或3!如果单纯用二进制枚举的话转移方程会十分甚至九分的复杂,而且状态还不好表示。
那我们就可以使用三进制,三进制的一个好处就是数字拓展到0,1,2,那么对于这个题来说,我们使用三进制枚举,按行自上往下递推。
那么就是以下2个合法状态:

我们从右往左按列枚举当前行,记当前位置为pos,分2种情况:
- 如果枚举到pos位,如果上一行对应的不是0,那么我们就要根据上图,填写相应的数,就是2→1,1→0。如果当前位置是坏的无法填写,那么排除。
- 如果是0,那么我们3种情况
- 填0直接跳过
- 填1,前提是右边3个都能够填写上1,这里判断方法其实和上面是一样的。
- 填2,右边2个要都能填写上2,同上
但是这么麻烦的情况,转移方程真的不太好得出。
没关系,DP2种实现方式,一种叫迭代,另一种叫记忆化搜索。
这里我们定义f[i][j]表示第i行填写的状态为j(j为三进制数)
那么就可以从i−1转移过来,但是这里的内存限制是8MB,没关系滚一滚就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| #include<bits/stdc++.h> using namespace std;
constexpr int MN=16; constexpr int NINF=-1061109567; int T,n,m,k; int f[2][1<<MN]; int pw[MN]; bool isbad[MN][MN];
int getn(int x,int pos){ return x%pw[pos]/pw[pos-1]; }
bool isok(int x,int lst,int pos){ if(!isbad[x][pos]&&!getn(lst,m-pos+1)) return 1; return 0; }
void dfs(int x,int lst,int now,int pos,int cnt){ if(!pos){ f[x%2][now] = max(f[x%2][now], f[(x-1)%2][lst] + cnt); return; } if(getn(lst,pos)){ if(isbad[x][m-pos+1]) return; if(getn(lst,pos)==2) dfs(x,lst,now*3+1,pos-1,cnt); else dfs(x,lst,now*3,pos-1,cnt); }else{ dfs(x,lst,now*3,pos-1,cnt); if(pos>=2 && isok(x,lst,m-pos+1) && isok(x,lst,m-pos+2)){ dfs(x,lst,(now*3+2)*3+2,pos-2,cnt+1); } if(pos>=3 && isok(x,lst,m-pos+1) && isok(x,lst,m-pos+2) && isok(x,lst,m-pos+3)){ dfs(x,lst,((now*3+1)*3+1)*3+1,pos-3,cnt+1); } } }
void solve(){ memset(isbad,0,sizeof(isbad)); memset(f,-0x3f3f3f3f,sizeof(f)); cin>>n>>m>>k; for(int i=1;i<=k;i++){ int x,y; cin>>x>>y; isbad[x][y]=1; } f[0][0]=0; for(int i=0;i<n;i++){ for(int j=0;j<pw[m];j++){ f[(i+1)%2][j]=NINF; } for(int j=0;j<pw[m];j++){ if(f[i%2][j]<0) continue; dfs(i+1,j,0,m,0); } } cout<<f[n%2][0]<<'\n'; }
int main(){ pw[0]=1; for(int i=1;i<MN;i++){ pw[i]=pw[i-1]*3; } cin>>T; while (T--){ solve(); } return 0; }
|
一般来说,三进制优化DP使用DFS来进行编写。