状态压缩DP(大佬写的很好,转来看)
奉上大佬博客 https://blog.csdn.net/accry/article/details/6607703
動態規劃本來就很抽象,狀態的設定和狀態的轉移都不好把握,而狀態壓縮的動態規劃解決的就是那種狀態很多,不容易用一般的方法表示的動態規劃問題,這個就更加的難于把握了。難點在于以下幾個方面:狀態怎么壓縮?壓縮后怎么表示?怎么轉移?是否具有最優子結構?是否滿足后效性?涉及到一些位運算的操作,雖然比較抽象,但本質還是動態規劃。找準動態規劃幾個方面的問題,深刻理解動態規劃的原理,開動腦筋思考問題。這才是掌握動態規劃的關鍵。
動態規劃最關鍵的要處理的問題就是位運算的操作,容易出錯,狀態的設計也直接決定了程序的效率,或者代碼長短。狀態轉移方程一定要仔細推敲,不可一帶而過,要思考為什么這么做,掌握一個套路,遇見這類問題能快速的識別出問題的本質,找出狀態轉移方程和DP的邊界條件。
下面是一些關于狀態壓縮DP的題目,大都不難。狀壓DP的東西還有很多,還會接著總結。
【POJ3254】Corn Fields
【題目大意】一個矩陣里有很多格子,每個格子有兩種狀態,可以放牧和不可以放牧,可以放牧用1表示,否則用0表示,在這塊牧場放牛,要求兩個相鄰的方格不能同時放牛,即牛與牛不能相鄰。問有多少種放牛方案(一頭牛都不放也是一種方案)
【解析】根據題意,把每一行的狀態用二進制的數表示,0代表不在這塊放牛,1表示在這一塊放牛。首先很容易看到,每一行的狀態要符合牧場的硬件條件,即牛必須放在能放牧的方格上。這樣就能排除一些狀態。另外,牛與牛之間不能相鄰,這樣就要求每一行中不能存在兩個相鄰的1,這樣也能排除很多狀態。然后就是根據上一行的狀態轉移到當前行的狀態的問題了。必須符合不能有兩個1在同一列(兩只牛也不能豎著相鄰)的條件。這樣也能去掉一些狀態。然后,上一行的所有符合條件的狀態的總的方案數就是當前行該狀態的方案數。
【狀態表示】dp[state][i]:在狀態為state時,到第i行符合條件的可以放牛的方案數
【狀態轉移方程】dp[state][i] =Sigma dp[state’][i-1] (state’為符合條件的所有狀態)
【DP邊界條件】首行放牛的方案數dp[state][1] =1(state符合條件) OR 0 (state不符合條件)
【代碼】
#include <cstdio> #include <cstring> using namespace std; #define mod 100000000 int M,N,top = 0; int state[600],num[110]; int dp[20][600]; int cur[20]; inline bool ok(int x){if(x&x<<1)return 0;return 1; } void init(){top = 0;int total = 1 << N;for(int i = 0; i < total; ++i){if(ok(i))state[++top] = i;} } inline bool fit(int x,int k){if(x&cur[k])return 0;return 1; } inline int jcount(int x) {int cnt=0;while(x){cnt++;x&=(x-1);}return cnt; }int main(){while(scanf("%d%d",&M,&N)!= EOF){init();memset(dp,0,sizeof(dp));for(int i = 1; i <= M; ++i){cur[i] = 0;int num;for(int j = 1; j <= N; ++j){scanf("%d",&num);if(num == 0)cur[i] +=(1<<(N-j));}}for(int i = 1;i <= top;i++){if(fit(state[i],1)){dp[1][i] = 1;}}for(int i = 2; i <= M; ++i){for(int k = 1; k <= top; ++k){if(!fit(state[k],i))continue;for(int j = 1; j <= top ;++j){if(!fit(state[j],i-1))continue;if(state[k]&state[j])continue;dp[i][k] = (dp[i][k] +dp[i-1][j])%mod;}}}int ans = 0;for(int i = 1; i <= top; ++i){ans = (ans + dp[M][i])%mod;}printf("%d\n",ans);} }【POJ1185】炮兵陣地–經典狀壓DP
【題目大意】類似于上面一道題,一個方格組成的矩陣,每個方格可以放大炮用0表示,不可以放大炮用1表示(原題用字母),讓放最多的大炮,大炮與大炮間不會互相攻擊。
【解析】可以發現,對于每一行放大炮的狀態,只與它上面一行和上上一行的狀態有關,每一行用狀態壓縮的表示方法,0表示不放大炮,1表示放大炮,同樣的,先要滿足硬件條件,即有的地方不能放大炮,然后就是每一行中不能有兩個1的距離小于2(保證橫著不互相攻擊),這些要預先處理一下。然后就是狀態表示和轉移的問題了,因為是和前兩行的狀態有關,所以要開個三維的數組來表示狀態,當前行的狀態可由前兩行的狀態轉移而來。即如果當前行的狀態符合前兩行的約束條件(不和前兩行的大炮互相攻擊),則當前行的最大值就是上一個狀態的值加上當前狀態中1的個數(當前行放大炮的個數)
【狀態表示】dp[i][j][k] 表示第i行狀態為k,第i-1狀態為j時的最大炮兵個數。
【狀態轉移方程】dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]); num[t]為t狀態中1的個數
【DP邊界條件】dp[1][1][i] =num[i] 狀態i能夠滿足第一行的硬件條件(注意:這里的i指的是第i個狀態,不是一個二進制數,開一個數組保存二進制狀態)
【代碼】
#include <cstdio> #include <cstring> using namespace std; #define max(a,b) (a) > (b) ? (a) : (b)int N,M; char map[110][20],num[110],top; int stk[70],cur[110]; int dp[110][70][70];inline bool ok(int x){ //判斷該狀態是否合法,即不存在相鄰的1之間的距離小于3的if(x&(x<<1)) return 0;if(x&(x<<2)) return 0;return 1; } //找到所有可能的合法狀態,最多60種 inline void jinit() {top=0;int i,total=1<<N;for(i=0;i<total;i++) if(ok(i)) stk[++top]=i; } //判斷狀態x是否與第k行匹配 inline bool fit(int x,int k) {if(cur[k]&x) return 0;return 1; } //數一個整型數x的二進制中1的個數(用于初始化) inline int jcount(int x) {int cnt=0;while(x){cnt++;x&=(x-1);}return cnt; }int main(){while(scanf("%d%d",&M,&N) != EOF){if(N == 0 && M == 0)break;jinit();for(int i = 1; i <= M; ++i)scanf("%s",map[i]+1);for(int i = 1; i <= M; ++i)for(int j = 1; j <= N; ++j){cur[i]=0;for(j=1;j<=N;j++){if(map[i][j]=='H')cur[i]+=(1<<(j-1));}}memset(dp,-1,sizeof(dp));//初始化第一行狀態for(int i = 1;i <= top;i++){num[i]=jcount(stk[i]);if(fit(stk[i],1))dp[1][1][i]=num[i];}int i,t,j,k;for(i = 2;i <= M;i++){for(t = 1;t <= top;t++){if(!fit(stk[t],i)) continue;for(j = 1;j <= top;j++){if(stk[t]&stk[j])continue;for(k = 1;k <= top;k++){if(stk[t]&stk[k])continue;if(dp[i-1][j][k]==-1)continue;dp[i][k][t] =max(dp[i][k][t],dp[i-1][j][k]+num[t]);}}}}int ans = 0;for(i = 1; i <= M; ++i)for(j = 1; j <= top; ++j)for(k = 1; k <= top; ++k)ans = max(ans,dp[i][j][k]);printf("%d\n",ans);}return 0; }【POJ3311】Hie With The Pie
【題目大意】類似于TSP問題,只是每個點可以走多次,比經典TSP問題不同的是要先用弗洛伊的預處理一下兩兩之間的距離。求最短距離。
【解析】可以用全排列做,求出一個最短的距離即可。或者用狀態壓縮DP.用一個二進制數表示城市是否走過
【狀態表示】dp[state][i]表示到達i點狀態為state的最短距離
【狀態轉移方程】dp[state][i] =min{dp[state][i],dp[state’][j]+dis[j][i]} dis[j][i]為j到i的最短距離
【DP邊界條件】dp[state][i] =dis[0][i] state是只經過i的狀態
【代碼】
#include<iostream> #define INF 100000000 using namespace std; int dis[12][12]; int dp[1<<11][12]; int n,ans,_min; int main() { //freopen("in.txt","r",stdin); while(scanf("%d",&n) && n) { for(int i = 0;i <= n;++i) for(int j = 0;j <= n;++j) scanf("%d",&dis[i][j]); for(int k = 0;k <= n;++k) for(int i = 0;i <= n;++i) for(int j = 0;j <=n;++j) if(dis[i][k] + dis[k][j]< dis[i][j]) dis[i][j] = dis[i][k] +dis[k][j]; for(int S = 0;S <= (1<<n)-1;++S)//枚舉所有狀態,用位運算表示 for(int i = 1;i <= n;++i) { if(S & (1<<(i-1)))//狀態S中已經過城市i { if(S ==(1<<(i-1))) dp[S][i] =dis[0][i];//狀態S只經過城市I,最優解自然是從0出發到i的dis,這也是DP的邊界else//如果S有經過多個城市 { dp[S][i] = INF; for(int j = 1;j <=n;++j) { if(S &(1<<(j-1)) && j != i)//枚舉不是城市I的其他城市 dp[S][i] =min(dp[S^(1<<(i-1))][j] + dis[j][i],dp[S][i]); //在沒經過城市I的狀態中,尋找合適的中間點J使得距離更短} } } } ans = dp[(1<<n)-1][1] + dis[1][0]; for(int i = 2;i <= n;++i) if(dp[(1<<n)-1][i] + dis[i][0] < ans) ans = dp[(1<<n)-1][i] +dis[i][0]; printf("%d\n",ans); } return 0; }【HDU3001】Traveling
【題目大意】10個點的TSP問題,但是要求每個點最多走兩邊,不是只可以走一次,所以要用三進制的狀態壓縮解決這個問題。可以預處理每個狀態的第k位是什么。
【解析】和tsp問題相同,類似于上面那個題
【狀態表示】【狀態轉移方程】同上題,具體見代碼
【代碼】
#include <cstdio> #include <cstring> #define INF 0x1f1f1f1f //剛發現這里寫0x1f1f1f跑的比0x1f1f1f1f差不多慢了一倍!Orz~ #define min(a,b) (a) < (b) ? (a) : (b) using namespace std;int N,M; int tri[12] ={0,1,3,9,27,81,243,729,2187,6561,19683,59049}; int dig[59050][11]; //dig[state][k_dig] 狀態state的第k位是多少 int edge[11][11],dp[59050][11];int main(){for(int i = 0; i < 59050; ++i){int t = i;for(int j = 1; j <= 10; ++j){dig[i][j] = t%3;t /= 3;if(t == 0)break;}}while(scanf("%d%d",&N,&M) != EOF){memset(edge,INF,sizeof(edge));int a,b,c;while(M --){scanf("%d%d%d",&a,&b,&c);if(c < edge[a][b])edge[a][b] = edge[b][a] = c;}memset(dp,INF,sizeof(dp));for(int i = 1; i <= N; ++i)dp[tri[i]][i] = 0;int ans = INF;for(int S = 0; S < tri[N+1]; ++S){int visit_all = 1;for(int i = 1; i <= N; ++i){if(dig[S][i] == 0)visit_all = 0;if(dp[S][i] == INF)continue;for(int j = 1; j <= N; ++j){if(i == j)continue;if(edge[i][j] == INF ||dig[S][j] >= 2)continue;int newS = S + tri[j];dp[newS][j] =min(dp[newS][j],dp[S][i] + edge[i][j]);}}if(visit_all){for(int j = 1; j <= N; ++j)ans = min(ans,dp[S][j]);}}if(ans == INF){puts("-1");continue;}printf("%d\n",ans);}return 0; }【POJ2288】Islands and Bridge
【題目大意】求漢密爾頓的一道變形問題,中間每個點有權值,關于最后得分的描述如下
Suppose there are n islands. The value of aHamilton path C1C2…Cn is calculated as the sum of three parts. Let Vi be thevalue for the island Ci. As the first part, we sum over all the Vi values foreach island in the path. For the second part, for each edge CiCi+1 in the path,we add the product ViVi+1. And for the third part, whenever three consecutiveislands CiCi+1Ci+2 in the path forms a triangle in the map, i.e. there is abridge between Ci and Ci+2, we add the product ViVi+1*Vi+2.
這題要求讓得分最高
【解析】發現每個點的狀態由前面兩個點確定,用DP(S,A,B)表示狀態為S時,當前到達A,而上一個點是B時的最大得分,這個狀態由DP(S’,B,C)通過從B走到A得到,S’=S-(1<<A),即S’狀態就是經過B和C但不經過A的一個狀態,C是不同于A和B的一個點。
【狀態轉移】dp[S][A][B] =max(dp[S][A][B],dp[S’][B][C]+temp) 這里的temp指的是加上的得分即VbVa+Va,如果構成三角關系(即A和C間有邊),temp就要再加上VbVa*Vc.
【邊界條件】DP((1<<A)+(1<<B),A,B)=Va+Vb+Va*Vb(A和B間有邊)表示
【代碼】
#include <cstdio> #include <cstring> using namespace std; const int MAXN = 13; const int MAX_S = 1<<(MAXN+1); long long dp[MAX_S][MAXN+1][MAXN+1]; long long way[MAX_S][MAXN+1][MAXN+1]; int edge[MAXN+1][MAXN+1]; long long V[MAXN+1];int N,M; int main(){int cas;scanf("%d",&cas);while(cas --){memset(edge,0,sizeof(edge));scanf("%d%d",&N,&M);for(int i = 1; i <= N; ++i)scanf("%d",&V[i]);if(N == 1){printf("%d 1\n",V[1]);continue;}int a,b;while(M --){scanf("%d%d",&a,&b);edge[a][b] = edge[b][a] = 1;}memset(dp,-1,sizeof(dp));memset(way,0,sizeof(way));int ii,jj;long long temp;for(int i = 1; i <= N; ++i)for(int j = 1; j <= N; ++j){if(i == j || !edge[i][j])continue;ii = 1<<(i-1);jj = 1<<(j-1);temp = V[i]+V[j]+V[i]*V[j];dp[ii+jj][i][j] = temp;way[ii+jj][i][j] = 1;}for(int S = 0; S < (1<<N); ++S)for(int i = 1; i <= N; ++i){if((S&(1<<(i-1))) == 0)continue;for(int j = 1; j <= N; ++j){if((S&(1<<(j-1))) ==0 || i == j || !edge[i][j])continue;for(int k = 1; k <= N; ++k){if(i == k || j == k ||(S&(1<<(k-1))) == 0)continue;int newS = S -(1<<(i-1));if(dp[newS][j][k] ==-1)continue;if(!edge[j][k])continue;temp =V[i]+V[i]*V[j]+dp[newS][j][k];if(edge[i][k])temp +=V[i]*V[j]*V[k];if(dp[S][i][j] < temp){dp[S][i][j] = temp;way[S][i][j] =way[newS][j][k]; //如果dp更新,way直接更新}//如果dp不更新,但dp=temp,way加上原來的值else if(temp ==dp[S][i][j])way[S][i][j] += way[newS][j][k];}}}long long ans = -1,num = 0;int p = (1<<(N)) - 1;for (int i = 1; i <= N; ++i)for (int j = 1; j <= N; ++j){if(i == j)continue;if (ans < dp[p][i][j]){ans = dp[p][i][j];num = way[p][i][j];}else if (ans == dp[p][i][j])num += way[p][i][j];}if(ans == -1){puts("0 0");continue;}printf("%lld %lld\n",ans,num/2);}return 0; }【ZOJ4257】MostPowerful
【題目大意】不超過10種氣體,兩兩之間相互碰撞可以產生一定的能量,如a碰b,那么b氣體就消失,自身不能碰自身,問最后所能得到的最大能量。
【題目解析】用10位二進制表示氣體是否存在,0表示存在,1表示不存在,S(上一個狀態)中的兩種氣體碰撞并且有一種消失,可以得到newS的狀態(狀態轉移)
【狀態表示】dp[state] 狀態為state時的最大能量
【轉移方程】dp[state] = max(dp[state],dp[state’]+a[i][j])
【邊界條件】dp[i] = 0;
【代碼】
#include <cstdio>#include <cstring>using namespace std;#define max(a,b) (a) > (b) ? (a) : (b)const int MAXN = 10;const int MAX_S = 1 << 10;int a[MAXN+1][MAXN+1];int dp[MAX_S];int N;int main(){while(scanf("%d",&N) != EOF){if(N == 0)break;for(int i = 0; i < N; ++i)for(int j = 0; j < N; ++j){scanf("%d",&a[i][j]);}memset(dp,0,sizeof(dp));int full = 1 << N;for(int s = 0; s < full; ++s){for(int i = 0; i < N; ++i){if((s&(1<<i)))continue;for(int j = 0; j < N; ++j){if(i == j)continue;if( (s&(1<<j)) )continue;int newS = s + (1<<j);dp[newS] = max(dp[newS],dp[s] + a[i][j]);}}}int ans = 0;for(int s = 0; s < full ; ++s)ans = max(ans,dp[s]);printf("%d\n",ans);}return 0;}【POJ2411】Mondriaan’sDream
【題目大意】一個矩陣,只能放1*2的木塊,問將這個矩陣完全覆蓋的不同放法有多少種。
【解析】如果是橫著的就定義11,如果豎著的定義為豎著的01,這樣按行dp只需要考慮兩件事兒,當前行&上一行,是不是全為1,不是說明豎著有空(不可能出現豎著的00),另一個要檢查當前行里有沒有橫放的,但為奇數的1。
【狀態表示】dp[state][i]第i行狀態為state時候的方案數
【轉移方程】dp[state][i] += dp[state’][i-1] state’為i-1行的,不與i行狀態state沖突的狀態
【邊界條件】第一行 符合條件的狀態記為1 即dp[state][0] = 1;
【代碼】
#include <cstdio>#include <cstring>using namespace std;int M,N;long long dp[1<<11][11];bool kexing[1<<11];int full;inline bool check(int in){int bit=0;while(in>0){if( (in&1)==1)bit++;else{if( (bit&1)==1)return false;bit=0;}in>>=1;}if( (bit&1)==1)return false;return true;}inline bool check2(int x1,int x2){if( (x1|x2)!= full-1 ) //如果這里不用位運算,時間很長,要用約3000MSreturn false;return kexing[x1&x2];}int main(){full = 1<<11;for(int s = 0; s < full; ++s)if(check(s))kexing[s] = 1;while(scanf("%d%d",&M,&N) != EOF){if(N == 0 && M == 0)break;memset(dp,0,sizeof(dp));full = 1<<N;for(int s = 0; s < full; ++s){if(kexing[s])dp[s][0] = 1;}for(int i = 1; i < M; ++i){for(int s = 0; s < full; ++s){for(int s1 = 0; s1 < full; ++s1){if(!check2(s1,s))continue;//if(dp[s1][i-1] == 0)continue; /*這一步判斷的時間要大于位運算和+=的時間,如果先把這一步放在check2前面,1300多MS,如果放在check2后面,610多MS,如果不加這一步,560MS,但如果check2用的不是位運算,將這一步加在check2前面3000MS左右(水過),如果不加這一步,TLE*/dp[s][i] += dp[s1][i-1];}}}int S = (1<<N) - 1;printf("%lld\n",dp[S][M-1]);}return 0;}【HDU3681】PrisonBreak 狀態壓縮DP+BFS+二分答案
2010杭州賽區的題目,以現在的水平遇到這種題也就能想一下,賽場上動手寫這個題是不會的。前天做狀壓DP的時候又看到這個題,沒想起來怎么做,昨天看了一下解題報告開始下手,遇到了各種問題。調試了N久,終于過了。
【題目大意】機器人從F出發,走到G可以充電,走到Y關掉開關,D不能走進,要求把所有開關關掉,且電量最少,并求出該最小電量。
【題目解析】機器人從出發點出發要求走過所有的Y,因為點很少,所以就能想到經典的TSP問題(起初我也想到了),但關于G點(不要YY)能充電的問題不知道怎么辦,看了下解題報告才知道。G點可以充電,到達G點就把當前能量更新為電池容量然后繼續走。因為每個G點只能充一次電,這就好像TSP中的每個點只能走一次一樣(G和Y都可以走多次,但走到G充電后,該點就變為了S,而走到Y關上開關以后,Y也變成了S。這是一個很巧妙地想法,所以要求Y點只能關一次開關,G點只能充一次電,這就是TSP了。Orz賽場上可以秒殺這題的大神們),然后就是二分答案了,用狀壓DP判定當前電池容量的情況下是否能符合條件。
【狀態表示】dp[s][i]表示到達當前i點狀態為s時最大的剩余的能量
【轉移方程】同TSP問題了
【邊界條件】dp[1<<sid][sid] = rongliang.即出發點的能量就是電池容量
【代碼】:
#include <cstdio>#include <cstring>#include <cmath>#include <queue>using namespace std;#define INF 0x1f1f1f1fint dp[32769][16];int dist[16][16][16][16];int di[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};int M,N,sid,nCnt,FinalState;char map[16][16];struct node{int x,y;node(){}node(int _x,int _y):x(_x),y(_y){}}nodes[16];inline void BFS(node start){queue<node> que;int sx = start.x,sy = start.y;dist[sx][sy][sx][sy] = 0;que.push(start);node cur;while(!que.empty()){cur = que.front();que.pop();int x = cur.x,y = cur.y,tx,ty;for(int i = 0; i < 4; ++i){tx = x + di[i][0];ty = y + di[i][1];if(tx < 0 || tx >= M || ty < 0 || ty >= N || map[tx][ty] == 'D')continue;if(dist[sx][sy][tx][ty] == -1){dist[sx][sy][tx][ty] = dist[sx][sy][x][y] + 1;que.push(node(tx,ty));}}}}inline bool ok(int s,int t){ //s狀態中走過所有t狀態中的城市if(((s&t)&t) == t)return 1;return 0; } inline bool check(int step){ int res = -1; memset(dp,-1,sizeof(dp)); dp[1<<sid][sid] = step; int full = 1<<nCnt; for(int s = 0; s < full; ++s){for(int i = 0; i < nCnt; ++i){ if((s&(1<<i)) == 0 || dp[s][i] == -1)continue; if(ok(s,FinalState))res = max(res,dp[s][i]); for(int j = 0; j < nCnt; ++j){ int temp = dist[nodes[i].x][nodes[i].y][nodes[j].x][nodes[j].y]; if(i == j || temp == -1 || (s&(1<<j)))continue; temp = dp[s][i] - temp; if(temp < 0)continue; int newS = s + (1<<j); dp[newS][j] = max(dp[newS][j],temp); if(map[nodes[j].x][nodes[j].y] == 'G')dp[newS][j] = step;}}}if(res < 0)return 0;return 1;}inline int solve(){int low = 0,high = 300;int mid;while(low <= high){mid = (low+high)/2;if(check(mid))high = mid-1;else low = mid+1;}if(low == 301)return -1;return low;}int main(){while(scanf("%d%d",&M,&N) != EOF){if(M == 0 && N == 0)break;nCnt = 0;FinalState = 0;for(int i = 0; i < M; ++i){scanf(" %s",map[i]);for(int j = 0; j < N; ++j){if(map[i][j] == 'F'){sid = nCnt;nodes[nCnt++] = node(i,j);FinalState += (1<<sid);}else if(map[i][j] == 'G')nodes[nCnt++] = node(i,j);else if(map[i][j] == 'Y'){int tid = nCnt;nodes[nCnt++] = node(i,j);FinalState += (1<<tid);}}}memset(dist,-1,sizeof(dist));for(int i = 0; i < nCnt; ++i)BFS(nodes[i]);int ans = solve();printf("%d\n",ans);}return 0;}總結
以上是生活随笔為你收集整理的状态压缩DP(大佬写的很好,转来看)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: POJ1088 滑雪题解+HDU 107
- 下一篇: HDU 4283 You Are the