状态压缩类动态规划,顾名思义,其核心就是在于对状态表达进行压缩。
在动态规划中,重点、难点更是突破点所在,就是阶段划分过后的状态表达及转移。在一般动态规划中,如果要维护一个集合的状态,常常用int 或bool类型的数组进行表达与描述。如果这个集合包含的元素个数很少,我们就把描述集合状态的数组压缩到一个整数中,这种用一个整数来描述一个集合的方法就叫作“状态压缩”。而当状态的某个维度存储的是一个集合的状态,这类问题就称作:状态压缩动态规划。
如果集合中每个元素的状态只有两种,我们一般用1和0进行描述,因此当我们用一个整数表达时,就自然而然地与二进制相对应。也就是说我们可以用一个整数对应的二进制位来表示集合中元素的状态。(如果状态有三种就采用三进制,以此类推)
比如给定一个整数23,它所对应的的二进制数为10111,那么我们就可以认为这个集合中包含了第1、2、3、5这些元素,而不包括第4个元素。
由此,我们在对整数的二进制位进行操作时,需要熟练掌握位运算的相关知识。这里给出状态压缩中常用的几个表达式:
判断一个数字x二进制第i位是否为1
mask&(1<<i)>0
将一个数字x二进制第i位更改为1
x|=(1<<i)
将一个数字x二进制第i位更改为0
x=x^(1<<i)
把一个数字二进制下最靠右的第一个1去掉
x=x&(x-1)
mask1是mask2的子集
(mask1&mask2)==mask1
n个位置所占的位数
(1<<n)-1
这里注意:位运算的运算级别非常低(甚至低于“==”),所以在使用位运算时可以说能加括号就加括号,不管括号是不是多余
状态操作
在进行状态压缩动态规划时,其基本思路同普通的DP没有太大区别,所以在进行状态转移时也要枚举各个状态,这就涉及到了对二进制表示状态时的操作(如对不同状态的枚举、遍历,子集枚举等)
1、枚举所有状态
for(int mask=1;mask<(1<<n);mask++)
{
}
2、枚举子集
以11(1011)为例:
int mask=11;//1011
for(int i=mask;i;i=(i-1)&mask)
{
for(int j=3;j>=0;j--)//11的二进制位有4位,所以从3—0枚举所有二进制位
{
if((i>>j)&1) cout<<1;
else cout<<0;
}
cout<<endl;
}
证明i=(i-1)&mask:
状态压缩动态规划的本质其实还是动态规划,所以搞定了用二进制表示状态后,直接与状态规划的思路接轨即可。
直接上例题:
例题
H. [Atcoder ABC187F] Close Group
题目描述
给定一个 N 个点, M 条边的无向图, 题目中保证没有重边.点集标记为 1,2,3,....n. 第 i 条边连接两个点Ai 和 Bi。
删去任意多条边,求最少能把整个图划分成若干个满足以下条件的连通分量.
条件:连通分量中任意两个点 a, b (1 ≤ a ≤ b), 都有: a, b之间有一条直接相连的边.
输入格式
第一行输入两个整数 N 和 M
接下来输入M行,每行包含两个整数 Ai 和 Bi 表示 Ai 和 Bi 间建立了一条边
输出格式
输出一个整数表示答案
样例数据
input1
3 2 1 2 1 3
output1
2
input2
4 6 1 2 1 3 1 4 2 3 2 4 3 4
output2
1
input3
10 11 9 10 2 10 8 9 3 4 5 8 1 8 5 6 2 5 3 6 6 9 1 9
output3
5
input4
18 0
output4
18
样例解释
对于样例1:移除顶点 1 和 2 之间的边或 移除顶点 1 和 3 之间的边
数据规模与约定
(1≤n≤18\)
(1≤m≤N∗(N−1)/2\)
时间限制:3s
空间限制:1024MB
首先考虑状态的设计,因为整个图的状态是由多个符合题意的连通分量构成的,所以应该先把各个点集划分的方案求出来,再合并出更大的点集,由此可以想到枚举子集的状压DP。
状态:dp[mask]表示选出的点集状态时mask时,划分的连通分量的个数集合中的最小值,当i位取1时表示该点被选入点集,反之不在点集中。
初值:dp[0]=0,没有点的时候不需要划分
不妨对每个点u预处理出一个集合的状态,表示u可以到达的点的集合,记作fa[u],具体判断时,只需要关注mask里面包含的每个点是否都出现在fa[u]中,即
if((fa[u]&mask)==mask)
具体状态转移时,考虑把mask集合分割成两个子集,套用枚举子集的模板,枚举mask的每一个子集,若分割的一个子集为j,则另一个就是mask-j,所以状态转移方程就是
dp[mask]=min(dp[mask],dp[j]+dp[mask-j]);
具体代码如下:
#include<bits/stdc++.h>
using namespace std;
int qr()
{
int x=0,f=0;char ch=0;
while(!isdigit(ch)) {f|=ch=='-';ch=getchar();}
while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return f? -x:x;
}
int n,m;
int dp[1<<20];
int fa[20];
void init()
{
n=qr();m=qr();
for(int i=0;i<n;i++)
fa[i]|=(1<<i);
for(int i=1;i<=m;i++)
{
int x,y;
x=qr();y=qr();
fa[y-1]|=(1<<(x-1));//注意点的下标问题,要上下匹配
fa[x-1]|=(1<<(y-1));
}
}
void work()
{
memset(dp,10,sizeof(dp));
dp[0]=0;
for(int mask=0;mask<(1<<n);mask++)
{
bool flag=1;
for(int j=0;j<n;j++)
{
if(((1<<j)&mask)==(1<<j)&&(fa[j]&mask)!=mask)
flag=0;
}
if(flag==1) dp[mask]=1;
for(int j=mask;j;j=(mask&(j-1)))
{
dp[mask]=min(dp[mask],dp[mask-j]+dp[j]);
}
}
cout<<dp[(1<<n)-1];
}
int main()
{
init();
work();
}