5.5 状压dp
基本概念
- 在状态比较多且可以用1/0表示时,往往使用状压dp解题
- 状压dp的状态设计往往形如 d p i , j dp_{i,j} dpi,j,表示考虑到第i行,状态为j时的方案数
- 其中j为一个二进制数,j的每一位表示对应行上每一列的状态
- 由于dp的一个维度是状态压缩产生的二进制数,所以说此类题目数据范围一般较小
- 在状压dp中,巧妙的位运算是AC的关键
位运算
详见位运算详解+竞赛常见用法总结
例题
例题1:种植方案
- 思路
由 n ≤ 12 n\le 12 n≤12可知,本题考察状压dp- 设 d p i , j dp_{i,j} dpi,j表示处理到第i行,田地状态为j时的方案数
- 显然,只有在j合法(为在相邻格子中种草)时,且上一行状态恰好与本行错开时才可以用i-1行更新i行
- 此时,有 d p i , j = ∑ d p i − 1 , k dp_{i,j}=\sum dp_{i-1,k} dpi,j=∑dpi−1,k(k为上一行的状态)
- 如何判断状态是否合法呢?
- 根据与运算的性质易得,对于一个10交替出现的二进制数,它和另一个恰好与他错开的01交替出现的二进制数进行按位与运算之后结果一定是0,反之一样成立
- 所以合法性的判断使用简单的位运算即可实现:对于相邻为1的判断,使用该二进制数与其左右移一位之后的数分别进行与(&)运算即可;对于上下两行状态合法性的判断同理,将两个状态进行与运算即可
- 像这样:
chk[i] = (!(i & (i >> 1))) && (!(i & (i << 1)));
预处理出这个判断数组后,当以状态数为下标时对应的值为一时,状态合法
- 代码:
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 20;
const int V = (1 << 12) + 10, p = 1e8;
int val[N], f[N][V], field[N][N];
bool chk[V];
int m, n;
signed main()
{
scanf("%lld%lld", &m, &n);
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
scanf("%lld", &field[i][j]);
}
}
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
val[i] = (val[i] << 1) + field[i][j];
}
}
int wall = 1 << n;
for (int i = 0; i < wall; i++)
{
chk[i] = (!(i & (i >> 1))) && (!(i & (i << 1)));
}
f[0][0] = 1;
for (int i = 1; i <= m; i++)
{
for (int j = 0; j < wall; j++)
{
if (chk[j]&&((j&val[i])==j))
{
for (int k = 0; k < wall; k++)
{
if (!(j & k))
{
f[i][j] = (f[i][j] + f[i - 1][k]) % p;
}
}
}
}
}
int ans=0;
for (int i = 0; i < wall; i++)
{
ans = (ans + f[m][i]) % p;
}
printf("%lld\n", ans);
return 0;
}
例题2:最短路径
- 思路:
- 观察Hamilton路径的定义,我们可以发现每个点的状态只能为0/1,故该状态可以使用一个二进制数来表示,故有dp状态:
d
p
i
,
s
dp_{i,s}
dpi,s表示已经走到i点,此时图中所有点的状态为s,则有转移:
d p i , s = m i n { d p k , ( s ˆ ( 1 < < i − 1 ) ) + d i s i , k } , k ≠ i 且 s & ( 1 < < k − 1 ) = = 0 dp_{i,s}=min\{dp_{k,(s\^\ (1<<i-1))}+dis_{i,k}\},k\neq i且s\&(1<<k-1)==0 dpi,s=min{dpk,(s ˆ(1<<i−1))+disi,k},k=i且s&(1<<k−1)==0
即走到k点(没有到过i点)的路径+i走到k的距离 - 注意状态中各点下标从0开始
- 代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 25;
int n,dis[N][N],f[N][(1<<21)];
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cin>>dis[i][j];
}
}
memset(f,0x3f,sizeof(f));
f[1][1]=0;
for(int j=0;j<(1<<n);j++){
for(int i=1;i<=n;i++){
if(!(j&(1<<i-1))) continue;
for(int k=1;k<=n;k++){
if(k==i) continue;
if(!(j&(1<<k-1))) continue;
f[i][j]=min(f[i][j],f[k][j^(1<<(i-1))]+dis[i][k]);
}
}
}
cout<<f[n][(1<<n)-1]<<'\n';
return 0;
}
练习1:最优组队
- 思路:
- 我们发现输入数据满足状压dp的格式,所以可以让输入直接成为dp数组的初值
- 接着只需枚举子集即可
- 一种独特的子集枚举方法:
- 设原状态为S,我们每次减去lowbit(S),同时&上S,这样就实现了枚举S中所有为1的位置的所有组合,也就是枚举出了拼成S表示的组合的所有方式
for(int i=1;i<=(1<<n)-1;i++){
int j=i;
while(j){
j-=i&(-i);
j=j&i;
dp[i]=max(dp[i],dp[j]+dp[i^j]);
}
}
- 代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 100;
int dp[N], n;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i <= (1 << n) - 1; i++) {
cin >> dp[i];
}
for (int i = 1; i <= (1 << n) - 1; i++) {
int j = i;
while (j) {
j -= i & (-i);
j = j & i;//注意每次要&上i
dp[i] = max(dp[i], dp[j] + dp[i ^ j]);
}
}
cout << dp[(1 << n) - 1] << '\n';
return 0;
}