1. 程式人生 > >動態規劃+狀態壓縮

動態規劃+狀態壓縮

轉載自->就是這裡

動態規劃+狀態壓縮

總述

狀態壓縮動態規劃,就是我們俗稱的狀壓DP,是利用計算機二進位制的性質來描述狀態的一種DP方式

很多棋盤問題都運用到了狀壓,同時,狀壓也很經常和BFS及DP連用,例題裡會給出介紹

有了狀態,DP就比較容易了

舉個例子:有一個大小為n*n的農田,我們可以在任意處種田,現在來描述一下某一行的某種狀態:

設n = 9;
有二進位制數 100011011(九位),每一位表示該農田是否被佔用,1表示用了,0表示沒用,這樣一種狀態就被我們表示出來了:見下表

列 數 1 2 3 4 5 6 7 8
二進位制 1 0 0 0 1 1 0 1
是否用 × × × ×

所以我們最多隻需要 2n+11

的十進位制數就好(左邊那個數的二進位制形式是n個1)

現在我們有了表示狀態的方法,但心裡也會有些不安:上面用十進位制表示二進位制的數,枚舉了全部的狀態,DP起來複雜度豈不是很大?沒錯,狀壓其實是一種很暴力的演算法,因為他需要遍歷每個狀態,所以將會出現2^n的情況數量,不過這並不代表這種方法不適用:一些題目可以依照題意,排除不合法的方案,使一行的總方案數大大減少從而減少列舉

位運算

有了狀態,我們就需要對狀態進行操作或訪問

可是問題來了:我們沒法對一個十進位制下的資訊訪問其內部儲存的二進位制資訊,怎麼辦呢?別忘了,作業系統是二進位制的,編譯器中同樣存在一種運算子:位運算 能幫你解決這個問題

(基礎,這裡不打算自己寫了,參照這篇部落格,以下內容也複製自qxAi的這篇部落格,這裡謝謝博主)

為了更好的理解狀壓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<<(i1))&x)>0)

 

將1左移i-1位,相當於製造了一個只有第i位上是1,其他位上都是0的二進位制數。然後與x做與運算,如果結果>0,說明x第i位上是1,反之則是0。

2.將一個數字x二進位制下第i位更改成1。

方法:x=x|(1<<(i1))

 

證明方法與1類似,此處不再重複證明。

3.把一個數字二進位制下最靠右的第一個1去掉。

方法:x=x&(x1)

 

感興趣的讀者可以自行證明。

位運算例題(結合BFS):P2622 關燈問題II

題目描述

現有n盞燈,以及m個按鈕。每個按鈕可以同時控制這n盞燈——按下了第i個按鈕,對於所有的燈都有一個效果。按下i按鈕對於第j盞燈,是下面3中效果之一:如果a[i][j]為1,那麼當這盞燈開了的時候,把它關上,否則不管;如果為-1的話,如果這盞燈是關的,那麼把它開啟,否則也不管;如果是0,無論這燈是否開,都不管。

現在這些燈都是開的,給出所有開關對所有燈的控制效果,求問最少要按幾下按鈕才能全部關掉。

輸入輸出格式

輸入格式:
前兩行兩個數,n m

接下來m行,每行n個數,a[i][j]表示第i個開關對第j個燈的效果。

輸出格式:
一個整數,表示最少按按鈕次數。如果沒有任何辦法使其全部關閉,輸出-1


這題需要對狀壓及位運算有一定的瞭解:首先要判斷某一位的燈是開的還是關的,才能進行修改。

具體解法是:對隊首的某一狀態,列舉每一個開關燈操作,記錄到達這一新狀態的步數(也就是老狀態 + 1),若是最終答案,輸出,若不是,壓入佇列。
也就是說:我們把初始狀態,用每個操作都試一遍,就產生了許多新的狀態,再用所有操作一一操作新狀態,就又產生了新的新狀態,我們逐一嘗試,直到有目標狀態為止,這可以通過BFS實現。

所以現在知道為什麼狀壓比較暴力了吧。

#include<iostream>
#include<vector> #include<queue> #include<cstdio> #include<cstring> using namespace std; int RD(){ int out = 0,flag = 1;char c = getchar(); while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();} while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();} return flag * out; } const int maxn = 2048; int num,m,numd; struct Node{ int dp,step; }; int vis[maxn]; int map[maxn][maxn]; void BFS(int n){ queue<Node>Q; Node fir;fir.step = 0,fir.dp = n;//初始狀態入隊 Q.push(fir); while(!Q.empty()){//BFS Node u = Q.front(); Q.pop(); int pre = u.dp; for(int i = 1;i <= m;i++){//列舉每個操作 int now = pre; for(int j = 1;j <= num;j++){ if(map[i][j] == 1){ if( (1 << (j - 1)) & now){ now = now ^ (1 << (j - 1));//對狀態進行操作 } } else if(map[i][j] == -1){ now = ( (1 << (j - 1) ) | now);//對狀態進行操作 } } fir.dp = now,fir.step = u.step + 1;//記錄步數 if(vis[now] == true){ continue; } if(fir.dp == 0){//達到目標狀態 vis[0] = true;//相當於一個標記flag cout<<fir.step<<endl;//輸出 return ;//退出函式 } Q.push(fir);//新狀態入隊 vis[now] = true;//表示這個狀態操作過了(以後在有這個狀態就不用試了) } } } int main(){ num = RD();m = RD(); int n = (1 << (num)) - 1; for(int i = 1;i <= m;i++){ for(int j = 1;j <= num;j++){ map[i][j] = RD(); } } BFS(n); if(vis[0] == false) cout<<-1<<endl; return 0; }

狀壓 + DP = 狀壓DP

同樣也是一種挺暴力的DP方式,我們直接看題吧

P1879 [USACO06NOV]玉米田Corn Fields

題目描述

Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can't be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.

Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.

農場主John新買了一塊長方形的新牧場,這塊牧場被劃分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一塊正方形的土地。John打算在牧場上的某幾格裡種上美味的草,供他的奶牛們享用。

遺憾的是,有些土地相當貧瘠,不能用來種草。並且,奶牛們喜歡獨佔一塊草地的感覺,於是John不會選擇兩塊相鄰的土地,也就是說,沒有哪兩塊草地有公共邊。

John想知道,如果不考慮草地的總塊數,那麼,一共有多少種種植方案可供他選擇?(當然,把新牧場完全荒廢也是一種方案)

輸入輸出格式

輸入格式:
第一行:兩個整數M和N,用空格隔開。

第2到第M+1行:每行包含N個用空格隔開的整數,描述了每塊土地的狀態。第i+1行描述了第i行的土地,所有整數均為0或1,是1的話,表示這塊土地足夠肥沃,0則表示這塊土地不適合種草。

輸出格式:
一個整數,即牧場分配總方案數除以100,000,000的餘數。


其實這題是可以減少狀態來達到減少複雜度的,可是我寫著題的時候還不會。。。關於減少複雜度可以看下面一篇例題

這題也是用二進位制來表示狀態,先預處理:列舉一行內(不考慮地圖因素)每一種狀態,看看是否合法,合法的話就打個標記(其實這裡可以減狀態數的那時候還不知道QAQ),以便後面操作,最後先處理第一行,列舉下面行的時候再列舉一遍上一行,看看兩行放置是否合法,合法就累計上一行計數即可。

#include<iostream>
#include<vector> #include<queue> #include<cstdio> #include<cstring> using namespace std; int RD(){ int out = 0,flag = 1;char c = getchar(); while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();} while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();} return flag * out; } const int maxn = 4096,M = 100000000; int n,m; int tmap[19][19]; int map[19]; int dp[19][maxn]; bool can[maxn]; int main(){ n = RD(),m = RD(); for(int i = 1;i <= n;i++){ for(int j = 1;j <= m;j++){ tmap[i][j] = RD(); map[i] = (map[i] << 1) + tmap[i][j];//利用把地圖變為二進位制的形式可以快速計算是否合法,要對位運算熟悉掌握 } } int maxstate = (1 << m) - 1;//最大狀態數 for(int i = 0;i <= maxstate;i++){ if((((i << 1) & i) == 0) & (((i >> 1) & i) == 0)){ can[i] = true;//後面有更優的寫法,看下一篇題目 } } for(int i = 0;i <= maxstate;i++){ if((can[i]) & ((i & map[1]) == i)){ dp[1][i] = 1;//先預處理出第一行(對於某一行的狀態,只受上一行影響) } } for(int i = 2;i <= n;i++){ for(int j = 0;j <= maxstate;j++){ if((can[j]) & (j & map[i]) == j){ for(int k = 0;k <= maxstate;k++){ if((k & j) == 0){ dp[i][j] = (dp[i][j] + dp[i - 1][k]) % M;//dp過程 } } } } } long long ans = 0; for(int i = 0;i <= maxstate;i++){ ans = (ans + dp[n][i]) % M;//答案在最後一行 } cout<<ans<<endl; return 0; }

P1896 [SCOI2005]互不侵犯King

題目描述

在N×N的棋盤裡面放K個國王,使他們互不攻擊,共有多少種擺放方案。國王能攻擊到它上下左右,以及左上左下右上右下八個方向上附近的各一個格子,共8個格子。

輸入輸出格式

輸入格式:
只有一行,包含兩個數N,K ( 1 <=N <=9, 0 <= K <= N * N)

輸出格式:
所得的方案數


題目十分簡短:觀察資料範圍我們可以知道:這題有龐大的狀態量,所以我們就用狀壓DP解決問題

dp思路:三維,第一維表示行數,第二維表示狀態(二進位制),第三維表示已經放了的棋子數(說實話做題做多了會有套路的,有數量限制的dp一般都要開一維表示用了的數量)

直接上程式碼:

#include<iostream>
#include<vector> #include<queue> #include<cstdio> #include<cstring> #define ll long long using namespace std; int RD(){ int out = 0,flag = 1;char c = getchar(); while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();} while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();} return flag * out; } int len,k; ll dp[19][1024][110]; int need[1024];//表示每種狀態用的棋子數 bool can[1024]; int main(){ len = RD();k = RD(); int maxstate = (1 << len) - 1; for(int i = 0;i <= maxstate;i++){ int temp = i; while(temp != 0){ if(temp % 2 == 1)need[i] += 1;//處理所需棋子數 temp /= 2; } } for(int i = 0;i <= maxstate;i++){ if(((i << 1) & i) == 0){ can[i] = true;//處理一行內不衝突的情況 } } for(int i = 0;i <= maxstate;i++){ if(can[i] & need[i] <= k)dp[1][i][need[i]] = 1;//預處理第一行 } for(int i = 2;i <= len;i++){ for(int j = 0;j <= maxstate;j++){ if(can[j]){ for(int s = 0;s <= maxstate;s++){ if(can[s] == false)continue; if((s & j) != 0)continue;//正面上我啊 if(((s << 1) & j) != 0)continue;//左邊上我啊 if(((s >> 1) & j) != 0)continue;//右邊上我啊 for(int l = k;l >= need[j];l--){ dp[i][j][l] += dp[i - 1][s][l - need[j]]; } } } } } ll ans = 0; for(int i = 0;i <= maxstate;i++){ ans += dp[len][i][k]; } cout<<ans<<endl; return 0; }

通過題目要求減少狀態量

這可以說是狀壓的一大精華了。一般狀壓的題目會有大量的狀態,列舉所有狀態則需要大量的時間,時間承受不了,若和dp結合起來,dp陣列開個三四維,空間也吃不消。

所以我們可以通過預處理狀態,去掉不合法的狀態,減少時空的需要

具體實現和STL中的map很相似:我們用一個序號來對映狀態,開一個數組INDEX[ ](這裡有坑,小寫的index會和cstring庫衝突,如果給用的話我絕對用小寫魔禁萬歲!!!(雖然我站上琴) )INDEX[i]表示第i個合法的狀態是什麼,然後列舉的時候直接列舉INDEX陣列就好了

P2704 [NOI2001]炮兵陣地

題目描述

司令部的將軍們打算在NM的網格地圖上部署他們的炮兵部隊。一個NM的地圖由N行M列組成,地圖的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示),如下圖。在每一格平原地形上最多可以佈置一支炮兵部隊(山地上不能夠部署炮兵部隊);一支炮兵部隊在地圖上的攻擊範圍如圖中黑色區域所示:

如果在地圖中的灰色所標識的平原上部署一支炮兵部隊,則圖中的黑色的網格表示它能夠攻擊到的區域:沿橫向左右各兩格,沿縱向上下各兩格。圖上其它白色網格均攻擊不到。從圖上可見炮兵的攻擊範圍不受地形的影響。 現在,將軍們規劃如何部署炮兵部隊,在防止誤傷的前提下(保證任何兩支炮兵部隊之間不能互相攻擊,即任何一支炮兵部隊都不在其他支炮兵部隊的攻擊範圍內),在整個地圖區域內最多能夠擺放多少我軍的炮兵部隊。

輸入輸出格式

輸入格式:
第一行包含兩個由空格分割開的正整數,分別表示N和M;

接下來的N行,每一行含有連續的M個字元(‘P’或者‘H’),中間沒有空格。按順序表示地圖中每一行的資料。N≤100;M≤10。

輸出格式:
僅一行,包含一個整數K,表示最多能擺放的炮兵部隊的數量。


自己推一下就可以發現,判斷此行是否合法需要列舉上一行和上兩行的狀態,(dp要開三維:第一維表示行數,第二維表示現在列舉的狀態,第三維表示上一行的狀態,所以dp[i][j][k]表示第i行排成j個狀態,且上一行狀態是k的最大數量),直接列舉所有狀態是肯定會超時的,這時候我們就需要通過題目要求減少狀態量了。

減少狀態量做法上面已經提過了,其他做法與普通狀壓類似。

總結一下此類題目的dp方法(玉米田也是這類問題):若某個狀態可以對下n行的狀態造成影響,那麼就要預處理前n行合法的,對於n + 1行及以後,判斷某狀態是否合法需要往上列舉n行,所以dp陣列要開n + 1維,第一維表示行數,第二維表示現在的狀態,再往後第n維表示上n - 2行的狀態(其實不可能出太多行的,時間指數增長)

這樣dp就這樣進行:

for(所有狀態)
    for(所有狀態)
        ...{向上列舉n行}
            dp[i][j][k][l]...[n + 1] += dp[i - 1][k][l]...[最上面一行];
            //求最大方案數就max() //意會吧,不怎麼講得清楚

AC程式碼:

#include<iostream>
#include<vector> #include<queue> #include<cstdio> #include<cstring> #define ll long long using namespace std; int RD(){ int out = 0,flag = 1;char c = getchar(); while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();} while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();} return flag * out; } const int maxn = 110; int lenx,leny; ll dp[110][maxn][maxn]; bool can[maxn]; bool cann[110][maxn]; int tmap[110][19]; int map[maxn]; int put[maxn]; int INDEX[maxn]; int cnt; char in; int main(){ lenx = RD();leny = RD(); for(int i = 1;i <= lenx;i++){ for(int j = 1;j <= leny;j++){ cin>>in; if(in == 'P')tmap[i][j] = 1; } } for(int i = 1;i <= lenx;i++){ for(int j = 1;j <= leny;j++){ map[i] = (map[i] << 1) + tmap[i][j];//和玉米田類似,處理為二進位制地圖 } } int maxstate = (1 << leny) - 1; for(int i = 0;i <= maxstate;i++){//列舉一行裡的狀態 if((((i << 1) & i) == 0) & (((i << 2) & i) == 0)){ INDEX[++cnt] = i;//合法的存在INDEX裡,最終cnt表示合法方案數 can[cnt] = true; int temp = i; while(temp != 0){ if(temp % 2 == 1){put[cnt] += 1;} temp /= 2; } } } for(int i = 1;i <= cnt;i++){//第一行 if(can[i] & ((INDEX[i] & map[1]) == INDEX[i])){ cann[1][i] = true; dp[1][i][0] = put[i]; } } for(int i = 1;i <= cnt;i++){ if(can[i] & ((INDEX[i] & map[2]) == INDEX[i])){//選一個第二行合法的 cann[2][i] = true;//標記一下合法,減少再計算 for(int j = 1;j <= cnt;j++){//在第一行找一個 if(!cann[1][j])continue;//要在第一行合法 if((INDEX[i] & INDEX[j]) == 0){//還要不與第二行衝突 dp[2][i][j] = max(dp[2][i][j],dp[1][j][0] + put[i]); } } } } for(int i = 3;i <= lenx;i++){ for(int j = 1;j <= cnt;j++){ if(can[j] & ((INDEX[j] & map[i]) == INDEX[j])){ cann[i][j] = true; for(int k = 1;k <= cnt;k++){//列舉上兩行狀態 if(!cann[i - 2][k])continue; if(!((INDEX[j] & INDEX[k]) == 0))continue; for(int l = 1;l <= cnt;l++){ if(!cann[i - 1][l])continue;//列舉上一行狀態 if(((INDEX[j] & INDEX[l]) != 0) || ((INDEX[k] & INDEX[l]) != 0))continue; dp[i][j][l] = max(dp[i][j][l],dp[i - 1][l][k] + put[j]); } } } } } ll ans = 0; for(int i = 1;i <= cnt;i++){ for(int j = 1;j <= cnt;j++){ ans = max(ans,dp[lenx][i][j]); } } cout<<ans<<endl; return 0; }