前言:
狀壓DP是一種非常暴力的做法(有一些可以排除某些狀態的除外),例如dp[S][v]中,S可以代表已經訪問過的頂點的集合,v可以代表當前所在的頂點為v。S代表的就是一種狀態(二進制表示),比如 (11001)2 代表在二進制中{0,3,4}三個頂點已經訪問過了,(11001)2 代表的十進制數就是25 ,所以當S為25的時候其實就是代表已經訪問過了{0,3,4}三個頂點,那假如一共有5個頂點(標號為01234)的話,所有的頂點都訪問完畢應該S為什么呢?是 (11111)2。
那么,在狀態轉移的過程中我們勢必要對十進制數轉化為二進制數的每一個的位數進行一些操作。下面,列舉了一些位運算的操作。

對於上面的一些操作舉例一些
就如之上的 (11001)2 來說是已經訪問了0,3,4三個頂點,當我接下去要訪問頂點2的話可以有以下操作:1.看看第3位是什么?那么選擇取出二進制數x的第k位,y = x >>(k-1) & 1,y = (110)2 & 1 = 0,2.把第3位變為1,y = x | (1<<(k-1)),y = (11001)2 | (100)2 = (11101)2,所以頂點2訪問完畢,加入到集合S中也結束了,此時S為(11101)2 = 29。
接下去就可以開始講一些例題了。最經典的就是白皮書上的tsp問題。
tsp問題
題意:假設有一個旅行商人要拜訪N個城市,他必須選擇所要走的路徑,路徑的限制是每個城市 只能拜訪一次,而且最后要回到原來出發的城市,要求路徑的總和最小。其中,2<=N<=15
思路:首先我們試着去設計狀態,假設現在已經訪問過的頂點的集合為S,當前所在頂點為v, 用dp[S][v]表示從v出發還有訪問剩余的所有頂點,最終返回到頂點0的路徑總和最小值。從v出發可以到任意一個還未訪問過的頂點,邊界就是訪問完所有頂點並且返回到頂點0。
遞推式為
dp[V][0] = 0;(邊界)
dp[S][v] = min{dp[S∪{u}][u] + d[u][v] | u不屬於S}
先通過記憶化搜索來看一下code
// s: 已經訪問過的節點狀態 v: 出發位置 int dfs(int s, int v) { if(dp[s][v]>=0) return dp[s][v]; if(s==(1<<n)-1 && v==0) //已經訪問過所有的節點並且再次回到了0起點 return dp[s][v]=0; int ans=INF; for(int u=0;u<n;u++) if(!(s>>(u &1))) // 如果u這位是0(代表沒有走過的話)就走u這個節點 ans=min(ans, dfs(s | (1<<u), u)+mp[v][u]); //dfs中第一個參數表示第u位變成1后的二進制數 return dp[s][v]=ans; } int main() { memset(dp, -1, sizeof(dp)); printf("%d\n", dfs(0, 0)); return 0; }
但是在這個問題中,對於任意兩個整數i和j,如果他們對應的集合滿足S(i)包含於S(j),就有i<=j,因此我們可以寫成循環的方式。
int dp[1<<maxn][maxn]; 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)){ //如果u不屬於S集合 dp[S][v] = min(dp[S][v],dp[S|1<<u][u]+d[v][u]); } } } } printf("%d\n",dp[0][0]); }
這樣子,一個狀壓DP的板子差不多就出來了。
