状态压缩动态规划(简称状压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。
方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)
将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i-1) )
证明方法与1类似,此处不再重复证明。
3.把一个数字二进制下最靠右的第一个1去掉。
方法:x=x&(x-1)
感兴趣的读者可以自行证明。
位运算在状压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; }