状态压缩类动态规划,顾名思义,其核心就是在于对状态表达进行压缩。
在动态规划中,重点、难点更是突破点所在,就是阶段划分过后的状态表达及转移。在一般动态规划中,如果要维护一个集合的状态,常常用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: