狀態壓縮動態規划(簡稱狀壓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; }