狀壓dp的含義
在我們解決動態規划題目的時候,dp數組最重要的一維就是保存狀態信息,但是有些題目它的具有dp的特性,並且狀態較多,如果直接保存的可能需要三維甚至多維數組,這樣在題目允許的內存下勢必是開不下的,那么我們能不能想個辦法,把它壓縮成一維呢?對,二進制.一般的動規題目數據范圍都不會太大,那么就可以把幾個狀態全部壓縮成一個二進制數保存下來,這樣就大大節省了空間,來允許我們進行其他的操作,這就叫做狀態壓縮.運用狀態壓縮來保存狀態的dp就叫做狀壓dp,這類dp一般數據范圍有一項很小(好像是不超過16吧),看到這種數據范圍就可以往狀壓上想
紙上談兵是沒用的,下面我們來看一道例題
題目大意:農夫有一塊地,被划分為m行n列大小相等的格子,其中一些格子是可以種植的(用1標記),農夫可以在這些格子里種植,其他格子則不能種植(用0標記),並且要求不可以使相鄰格子都被種植。現在輸入數據給出這塊地的大小及可否種植的情況,求該農夫有多少種種植方案可以選擇(注意:任何格子都不種植也是一種選擇,不要忘記考慮!)
解題思路:按照剛才我說的,題目中的m,n最大都只有12,我們要很快想到狀壓dp,那么如何狀壓呢?其實狀壓dp就是一種枚舉,是最暴力的一種dp.
在題目中,有1的地方就可以種植,否則不行,在不考慮時間復雜度的情況下,我們是不是會想到打暴搜,枚舉每一種情況,如果一塊地上已經種了草,那么上下左右就都不能種了.我們經這種思路轉化成二進制,1代表在這塊地上種植,0代表不種,例如:010就代表在第二塊地種植,其他地都不種.
我們枚舉每一行的狀態,在左右不相鄰的情況下,再判斷下一行不和本行狀態沖突的狀態(如:第一行是0 1 0,第二行是0 1 0就沖突了,即上下行同一位置不能同時種植),這樣我們只需要預處理出第一行的狀態就可以遞推出其他行的所有滿足條件的狀態個數了
下面來分析一下題目樣例
1 1 1
0 1 0
第一行滿足條件的狀態有
1 | 0 0 0 |
2 | 1 0 0 |
3 | 0 1 0 |
4 | 0 0 1 |
5 | 1 0 1 |
第二行滿足條件的狀態有
1 | 0 0 0 |
2 | 0 1 0 |
根據乘法原理有5*2=10種方法,但其中一種第一行0 1 0和第二行0 1 0是沖突的,所以結果為10-1=9種方案
設計dp數組的狀態,狀壓dp狀態應該還是比較好設計的,本題為dp[i][state[j]]表示到第i行到第j種狀態滿足條件的方案數
滿足無后效性原則,下一行的狀態只能由前一行轉移過來
dp[i+1][state[j]]+=dp[i][state[k]] state[k]表示第i行滿足條件的狀態
總結一下思路:先枚舉第一行,把所有可能的狀態和第一行的題目所給環境對比,如果成功,則在循環里繼續枚舉第二行,把所有可能的狀態和第二行的環境對比,如果成功,再和第一行填入的狀態對比,如果又匹配成功,則dp[2][000] = dp[2][000] + dp[1][100];方法數加到第二行。這就是一次循環結束了,重新枚舉第二行...
1 //cur[i]表示第i行的環境 2 for(int i=1;i<=m;i++) { 3 for(int j=1;j<=n;j++) { 4 int a; in(a);//輸入環境 5 if(!a) cur[i]|=(1<<(n-j));//這個有兩點要注意,一個是所有的0變成1,1變成0(這個必須),一個是反向(正向也可以)存環境------>cur[i]|=(1<<(j-1)); 6 } 7 }
我們假設一下如果不是0,1互換,那么我們后面判斷它是否合法時就會出現問題,比如我第1行的狀態為1 0 1,互換后為0 1 0,在后面的程序中有這樣一條判斷是否合法的語句
if((can[i] & cur[j])==0) 代表它合法--------互換后的程序
不互換的話就是這樣 if(can[i] & cur[j]) 乍一看好像沒什么不對,但是我們考慮一種情況,就是當我們枚舉的狀態為0時,后面的這一種語句是無法滿足要求的,但在題目中不種植也算一種方案,所以我們就需要0,1互換這個操作
1 for(int i=0;i<tot;i++) if(!(i&(i<<1))) can[++cnt]=i;//所有左右兩邊不相鄰的狀態
這是保存狀態的語句,tot=1<<n,n為列數.題目要求相鄰兩邊不能同時種植,我們就把一個狀態,左移一位也就是取它的下一位,再與它自己想與,若大於0,則代表有相鄰的1,否則就沒有.
這樣就巧妙的判斷了左右相鄰的情況
for(int i=1;i<=cnt;i++)if(!(cur[1]&can[i])) dp[1][can[i]]=1;//預處理第1行的可行狀態
只要預處理第1行就好了,后面的行數都是由它轉移而來的對吧
1 for(int i=1;i<m;i++) //枚舉1~m-1行 2 for(int j=1;j<=cnt;j++)//枚舉所有可行的狀態 3 if((cur[i]&can[j])==0)//如果第i行滿足環境要求 4 for(int k=1;k<=cnt;k++)//枚舉第i+1行的狀態 5 if(((can[k]&cur[i+1])==0) && ((can[j]&can[k])==0))//和第i+1行的狀態滿足第i+1行的環境以及不與的第i行狀態沖突 6 dp[i+1][can[k]]=(dp[i+1][can[k]]+(dp[i][can[j]]%mod))%mod;//狀態數相加
這就是本代碼的核心程序,處理出每一行滿足條件的方案數.
最后貼一下總代碼
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #include<cstdio> 5 #include<string> 6 #define in(i) (i=read()) 7 using namespace std; 8 const int mod=100000000; 9 int read() 10 { 11 int ans=0,f=1; char i=getchar(); 12 while(i<'0'||i>'9') {if(i=='-') f=-1; i=getchar();} 13 while(i>='0'&&i<='9'){ans=(ans<<1)+(ans<<3)+i-'0'; i=getchar();} 14 return ans*f; 15 } 16 int dp[13][1<<12],can[1<<12],cur[13]; 17 int main() 18 { 19 int m,n,cnt=0,ans=0,tot; 20 in(m);in(n); tot=1<<n; 21 //cur[i]表示第i行的環境 22 for(int i=1;i<=m;i++) { 23 for(int j=1;j<=n;j++) { 24 int a; in(a);//輸入環境 25 if(!a) cur[i]|=(1<<(n-j));//這個有兩點要注意,一個是所有的0變成1,1變成0(這個必須),一個是反向(正向也可以)存環境------>cur[i]|=(1<<(j-1)); 26 } 27 } 28 for(int i=0;i<tot;i++) if(!(i&(i<<1))) can[++cnt]=i;//所有左右兩邊不相鄰的狀態 29 for(int i=1;i<=cnt;i++)if(!(cur[1]&can[i])) dp[1][can[i]]=1;//預處理第1行的可行狀態 30 for(int i=1;i<m;i++)//枚舉1~m-1行 31 for(int j=1;j<=cnt;j++)//枚舉所有可行的狀態 32 if((cur[i]&can[j])==0)//如果第i行滿足環境要求 33 for(int k=1;k<=cnt;k++)//枚舉第i+1行的狀態 34 if(((can[k]&cur[i+1])==0) && ((can[j]&can[k])==0))//和第i+1行的狀態滿足第i+1行的環境以及不與的第i行狀態沖突 35 dp[i+1][can[k]]=(dp[i+1][can[k]]+(dp[i][can[j]]%mod))%mod;//狀態數相加 36 for(int i=1;i<=cnt;i++) 37 ans=(ans+dp[m][can[i]])%mod; 38 cout<<ans<<endl; 39 return 0; 40 }
狀壓dp的其他例題
2.Codefoces--Kefa and Dishes 題解
狀態壓縮十分有用,並不一定只能用於dp,有些范圍比較大的數據結構有時也需要狀壓,留待同學們以后做題時自己去發現