【POJ2411】Mondriaan's Dream-狀態壓縮DP(插頭DP?)
題目大意:求用1*2的骨牌完美覆蓋h*w的棋盤的方法數。
做法:這道題絕對是經典題啊,久仰大名......關於輪廓線和插頭的思想可以看cdq大大的論文:點這裡。但這題不用搞那麼麻煩,因為骨牌之間相互獨立,所以不用考慮插頭的連通性問題,直接無插頭為0,有插頭為1狀態壓縮即可。
首先如果h*w為奇數,直接輸出0。對於其他情況,用f[i][j][state]表示骨牌覆蓋前i-1行和第i行的前j列,且輪廓線狀態為state的方案數,其中state是一個w+1位的二進位制數。這裡如果狀態中一位為1,就表示這個位置有一個格子在等待著某一個狀態的另一個格子去接上,從而形成一塊骨牌,要注意的是輪廓線的彎折部分相鄰兩個插頭不能同為1,因為我們知道一個格子不可能同時有兩個位置的格子去接,一個格子也不可能同時接上兩個格子。
接下來考慮逐格遞推,當遞推到第i行第j列的格子時,考慮它是去接上一個格子從而拼成一塊骨牌,還是新開一個格子等待接下來的格子來接。列舉狀態,這時觀察輪廓線的外凸部分(想象一下,即當前格子的右邊線和下邊線),如果為“00”,則表示這個格子是去接上一個前面的格子,否則表示這個格子是新開的。對於接前面格子的情況,考慮什麼樣的狀態才能推到當前狀態,答案就是去掉當前格的輪廓線上,內凹部分(即當前格的左邊線和上邊線)某一個插頭是1,而其他插頭都不變的狀態。這樣只有兩個狀態能推到當前狀態,累加即可。對於新開格子的情況,考慮什麼樣的狀態才能推到當前狀態,答案是去掉當前格的輪廓線上,內凹部分插頭都是0,其他插頭都不變的狀態。這樣只有一個狀態能推到當前狀態,累加即可。最後對於每一行最後一列格子的狀態轉移特殊處理即可。可以知道,最後的答案就是f[h][w][0]。
這樣整個DP的時間和空間複雜度都是O(n^2*2^n),鑑於n最大隻達到11,所以完全可行。如果你覺得空間複雜度太高,可以用滾動陣列壓到O(2^n)。
以下是本人程式碼(初次寫,寫得很醜,見諒......):
#include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <algorithm> #define ll long long using namespace std; int n,m,now,past; ll f[2][3010],s[3010]; int main() { while(scanf("%d%d",&n,&m)&&n&&m) { if (n*m%2!=0) {printf("0\n");continue;} memset(f,0,sizeof(f)); f[0][0]=1; now=1,past=0; if (n<m) swap(n,m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { memset(f[now],0,sizeof(f[now])); for(int k=0;k<(1<<(m+1));k++) { int bit0,bit1,bit2; bit0=k&(1<<(j-1)); bit1=k&(1<<j); bit2=k&(1<<(j+1)); if ((bit0&&bit1)||(bit1&&bit2)) continue; if (!bit0&&!bit1) { f[now][k]+=f[past][k+(1<<(j-1))]; f[now][k]+=f[past][k+(1<<j)]; } else { if (bit0) f[now][k]+=f[past][k-(1<<(j-1))]; if (bit1) f[now][k]+=f[past][k-(1<<j)]; } } if (j==m) { memset(s,0,sizeof(s)); for(int k=0;k<(1<<m);k++) s[k<<1]=f[now][k]; for(int k=0;k<(1<<(m+1));k++) f[now][k]=s[k]; } swap(now,past); } printf("%lld\n",f[past][0]); } return 0; }