状态压缩DP入门
状态压缩动态规划(简称状压dp)是另一类非常典型的动态规划,通常使用在NP问题的小规模求解中,虽然是指数级别的复杂度,但速度比搜索快,其思想非常值得借鉴。
为了更好的理解状压dp,首先介绍位运算相关的知识。
1.’&’符号,x&y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如3(11)&2(10)=2(10)。
2.’|’符号,x|y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如3(11)|2(10)=3(11)。
3.’^’符号,x^y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如3(11)^2(10)=1(01)。
4.’<<’符号,左移操作,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。相应的,’>>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。
这四种运算在状压dp中有着广泛的应用,常见的应用如下:
一些二进制的操作
1. 如何判断数字x第i位是否为1 : 1<<(i-1) & x
(以整数x表示的集合,是否包含元素i)
2. 将一个数字x二进制下第i位更改成1 : x = x | (1<<(i-1))
(把元素i加入到集合x中)
3. 把一个数字二进制下最靠右的第一个1去掉 : x = x & (x-1)
(把元素i从集合x中删除)
4. 判断二进制数中不存在相邻的1
可以用移位并按位与的办法来判断,举个例子:110左移一位为011 ,110&011 = 1,不符合要求;101左移一位为010,101&010=0,符合要求,这是判断同一行时的方法。
i & ( i >> 1 ) ) == 0
5. 从集合中去掉第i个元素
s & ~ ( 1 << i )
6. 判断i是否属于集合S
if ( s >> i & 1 )
7. 判断上下两行有没有相邻的
只需将上下两行的状态按位与即可
if ( status[j] & status[k] ) 则不成立
感兴趣的读者可以自行证明。
位运算在状压dp中用途十分广泛,请看下面的例题。
定一个n个顶点的带权有向图的距离矩阵d(i,j)(INF表示没有边)。要求从顶点0出发,经过每个顶点恰好一次再回到顶点0,求所经过边的总权重的最小值。
这就是著名的旅行商问题(TSP,Travelling Salemans Problem)。TSP问题是NP困难的,没有已知的多项式时间的高效算法可以解决这一问题不过在程序设计中数据比较小的题目还是可能出现的。所有可能的路线总共有(n-1)!种,可以用dp来解决复杂度为2^n2。
假设现在已访问过的顶点的集合为S(顶点0当做未访问的顶点),当前所在顶点为v,用dp[S][v]表示从v出发访问剩余的所有顶点,最终回到顶点0的路径的权重总和的最小值。由于从v出发可以移动到任意的一个顶点u(u不属于S),因此有如下递推式:dp[V][0]=0,dp[S][v]=min{dp[S+u][u]+d[v][u]}。用这个递推式就可以计算出所求结果。不过在递推式中有一个下标不是整数而是集合,对于集合我们可以把每个元素的选取与否对应到一个二进制位里,从而把状态压缩成一个整数,大大简化了计算和维护。忘记说了,想学好状压dp,首先要熟练掌握位运算╮(╯_╰)╭。(这是在《挑战程序设计竞赛》里看到的)
AC:
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; #define INF 0x3f3f3f3f int m[21][21],n; int dp[1<<21][21]; void solve() { //用足够大的值初始化数组 for (int S = 0; S < 1 << n; S++){ fill(dp[S], dp[S] + n, INF); } dp[(1 << n) - 1][0] = 0; //根据递推式依次计算 for (int S = (1 << n) - 2; S >= 0; S--){ for (int v = 0; v < n; v++){ for (int u = 0; u < n; u++){ if (!(S >> u & 1)){ dp[S][v] = min(dp[S][v], dp[S | 1 << u][u] + m[v][u]); } } } } if(dp[0][0]!=INF) printf("%d\n", dp[0][0]); else puts("no Hamilton circuit"); } int main( ) { int e,u,v,w; while(scanf("%d%d",&n,&e)!=EOF) { memset(m,INF,sizeof(m)); for(int i=0 ; i<e ; i++) { scanf("%d%d%d",&u,&v,&w); if(m[u][v] >w ) m[v][u]=m[u][v]=w; } solve( ); } return 0; }