状态压缩DP
[TOC]
1.状态压缩的定义
状态压缩的定义:我们知道任何一个二进制都可以对应唯一的十进制数,反过来也成立。所以我们可以用一个数来代替一组数从而降低维数。这种解题手段我们叫做状态压缩。
举个例子:如果数组中的某一行全是0或全是1,例如000,001,我们可以将000用0表示,001用1来表示,这样一来就降低了维数,起到了状态压缩的作用。状态压缩dp就是基于此的dp。
2.位运算
状压dp与位运算关系密切,所以先了解一下位运算。
基本的一些位运算(如&,|,^,~,<<,>>)的基本用法就不多做介绍。来说一下它们的一些运用:
1:判断一个数x的二进制下第 i 位是不是1:(x & 1 << i)
2:将一个数x的二进制下第 i 位改为1:(x = x | (1 << i - 1))
3:把一个数二进制下最靠右的第一个1去掉:(x &= x - 1)
......待补充
3.入门例题
1:poj-3254
题意:一个n*m的草地,1代表有草,0有没。两个草不能相邻,问有多少种选法。
状态:dp[i][state[j]]
表示前 i 行,第 i 行采用第 j 个状态时得到的总方案数。所以可得状态转移方程:dp[i][state(j)]=dp[i-1][state(k1)]+dp[i-1][state(k2)]+......+dp[i-1][state(kn)]
。
show code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e8;
const int maxn=1e5+10;
int state[maxn],dp[15][maxn];
int arr[15][15],n,m,tot;
void init() //求出合理状态打个表
{
int maxlen=1<<12;
for(int i=0;i<maxlen;++i){
if((i&(i<<1))==0) state[++tot]=i; //与运算要打括号!!!
}
}
bool check(int row,int x)
{
for(int i=1;i<=m;++i)
{
if(x&(1<<(i-1))&&!arr[row][i]) //检查一下x的所有位是否与已求出的合法状态冲突
return false;
}
return true;
}
int main()
{
init();
while(scanf("%d%d",&n,&m)!=EOF)
{
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
scanf("%d",&arr[i][j]);
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;++i)
{
for(int j=1;state[j]<(1<<m);++j)
{
if(!check(i,state[j])) continue; //保证下面的情况都与输入不冲突
if(i==1){
dp[i][j]=1;
continue;
}
for(int k=1;state[k]<(1<<m);++k){ //根据状态转移方程而来
if((state[k]&state[j])==0) //同一列没有两个1,两个数做与运算一定要打括号!!!!
dp[i][j]+=dp[i-1][k];
}
}
}
/*for(int i=1;i<=n;++i){
for(int j=1;state[j]<(1<<m);++j)
printf("%d ",dp[i][j]);
printf("
");
}*/
ll res=0;
for(int i=1;state[i]<(1<<m);++i)
res=(res+dp[n][i])%mod;
printf("%d
",res);
}
return 0;
}
2:poj-3311
题意:一个有向图,一个点可以走多次,问从0号点开始走把所有点都走一遍且最后回到原点的最短路径是多少。
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=15;
int arr[maxn][maxn];
int n,dp[1<<11][maxn];
int main()
{
while(scanf("%d",&n)&&n)
{
for(int i=0;i<=n;++i)
for(int j=0;j<=n;++j)
scanf("%d",&arr[i][j]);
for(int k=0;k<=n;++k) //预处理求出任意两点之间的最小距离
for(int i=0;i<=n;++i)
for(int j=0;j<=n;++j)
if(arr[i][k]+arr[k][j]<arr[i][j])
arr[i][j]=arr[i][k]+arr[k][j];
for(int s=0;s<(1<<n);++s) //s表示状态
{
for(int i=1;i<=n;++i) //状态要从1开始表示,但其实是第i+1号节点(从1开始编号的话)
{
if(s&(1<<(i-1)))
{
if(s==(1<<(i-1))) dp[s][i]=arr[0][i]; //如果刚好只到城市i,也是边界情况
else{
dp[s][i]=inf;
for(int j=1;j<=n;++j) //找一个合适的点使转过去之后距离最小
if ((s>>(j-1))&1 && j!=i )
dp[s][i]=min(dp[s][i],dp[s^(1<<(i-1))][j]+arr[j][i]);
//在s状态没有经过城市i中找一点,使得距离更短
}
}
}
}
int res=dp[(1<<n)-1][1]+arr[1][0];
for(int i=2;i<=n;++i)
res=min(res,dp[(1<<n)-1][i]+arr[i][0]);
printf("%d
",res);
} s
}
3:poj 1185
题意:m*n的一块地, P表示平原, H表示山地, P上可以部署炮车, 炮车的攻击范围为前后左右范围内2个单位的十字行, 不能斜的攻击, 问最多能放多少个炮车。n : 100, m : 10;
因为每行只有10列, 故可用一个十进制数将所有状态都表示出来, 注意约束条件:①:山地不能放炮车, ②:炮车之间不能相互攻击;故可设$$f[i][j][k]$$表示第 i 行的状态为 j, 上一行的状态为 k 的可部署的最多炮车的数量。直接开100行数组会爆内存,加个滚动数组优化就行了
#include <bits/stdc++.h>
#define debug(x) cout << #x << " = " << x << endl;
using namespace std;
typedef long long LL;
const int maxn = 1 << 11;
const int inf = 0x3f3f3f3f;
const double eps = 1e-6, pi = acos(-1.0);
int gcd(int a,int b) { return b == 0 ? a : gcd(b, a % b); }
int n, m;
int f[2][maxn][maxn], g[110];
char s[110][11];
vector<int> state;
bool ok(int x){
for(int i = 0; 1 << i <= x; ++i){
if((x & 1 << i) && ((x & 1 << i + 1) || (x & 1 << i + 2))) return 0;
}
return 1;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 0; i < n; ++i){
scanf("%s", s[i]);
for(int j = 0; j < m; ++j){
if(s[i][j] == 'P') continue;
g[i] += 1 << j;
}
}
for(int i = 0; i < 1 << m; ++i){ //打表, 将符合条件的状态存起来
if(ok(i)) state.push_back(i);
}
int len = state.size();
for(int i = 0; i <= n + 1; ++i){ //阶段: 枚举到哪一行了
for(int j = 0; j < len; ++j){ //状态:上山一行的状态
for(int k = 0; k < len; ++k){ //状态:上一行的状态
for(int u = 0; u < len; ++u){ //状态:这一行的状态, 顺序可以乱
int now = state[u], pre = state[k], ppre = state[j];
if((now & pre) || (now & ppre) || (pre & ppre)) continue;
if(g[i] & now) continue;
f[i&1][now][pre] = max(f[i&1][now][pre], f[i-1&1][pre][ppre] + __builtin_popcount(now));
//决策, 这个状态可以从上一行的状态转移而来
}
}
}
}
printf("%d
", f[n+1&1][0][0]);
}