「POJ2411」Mondriaan's Dream 題解 (狀壓DP)
個人覺得\(\mbox{POJ}\)有點玄學(畢竟是北大),勿噴。
題目描述
求把 \(N\times M\) 的棋盤分成若干個 \(1\times 2\) 的長方形,有多少種方案。
分析
首先,如果 \(N\times M\in \{x|x=2k+1,k\in \mbox{Z}\}\) (\(N\times M\) 為奇數),直接下一個。
\(1\times 2\) 的骨牌可以有兩種覆蓋方法,橫著覆蓋或是豎著覆蓋。
橫著放只取決於目前一行的狀態,豎著放則依賴於上下兩行。
於是我們可以將其表示為二進位制,\(0\) 表示這一行這一位是完整的,\(1\) 表示不完整,需要上面或下面加一塊來補。
例如,對於一個 \(4\times 4\)
那麼第二排就可以這樣放:
\[1\ 1\ 1\ 1\ (15) \]合起來就是
\[1\ 1\ 0\ 0\ (12) \]\[1\ 1\ 1\ 1\ (15) \]不難發現,如果第一行某一位狀態為 \(1\) ,那麼第二行這一位狀態只能為 \(1\),如果第一行某一位狀態為 \(0\),那麼第二行這一位狀態可以任選。
不過,第三行怎麼放呢?
如果我們將前兩行的結果壓為第一行,那麼前兩行的狀態就是:
\[0\ 0\ 1\ 1\ (\ 3\ ) \]因為前兩行已經組成了一個完整的長方形,不缺了。
這個 \(\ 0\ 0\ 1\ 1\)如何得來呢?
不難發現:
\[[1\ 1\ 0\ 0]\ (12)\ \mbox{xor}\ [1\ 1\ 1\ 1]\ (15)\ = [0\ 0\ 1\ 1]\ (3) \]所以,這一個狀壓 \(\mbox{DP}\) 的解題鑰匙,就是 \(\mbox{xor}\) 運算。
狀態
\(f_{i,j}\) 覆蓋第 \(i\) 行後,第\(1\ to\ i\)行狀態為 \(j\) 的方案數。
目標
\(f_{n,0}\)
狀態轉移
\(f_{i,j\ \mbox{xor}\ k}+=f_{i-1,k} (\ (j\ \mbox{xor}\ k)\ \mbox{and}\ k\ =\ 0\ )\)
\(j\) 表示當前行狀態,\((j\ \mbox{xor}\ k)\ \mbox{and}\ k\ =\ 0\)
注意,\(j\) 必須是合法狀態,可以事先處理出來,而 \(k\) 是總狀態,必須列舉。
初始狀態
\(f_{1,j} = 1\)
\(j\) 是一個合法狀態
AC code
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9')ch=getchar();
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x;
}
int d[15][405];
int cnt[15];
void prep(){
for(int i=0;i<(1<<11);i++){//預處理合法狀態,將010一類不可能的狀態排除
int tot=0;
for(int j=0;j<11;j++){
int t=(i>>j)&1;
if(!t)tot++;
if(tot&1){
if(t)break;
continue;
}
if(i>(1<<j+1)-1)continue;
cnt[j+1]++;
d[j+1][cnt[j+1]]=i;
}
}
}
typedef long long ll;
ll f[15][(1<<11)+5];
int main(){
prep();
while(true){
int n=read(),m=read();
if(!n&&!m)break;
if((n&1)&&(m&1)){
puts("0");
continue;
}
memset(f,0,sizeof f);
for(int i=1;i<=cnt[m];i++)f[1][d[m][i]]=1;//初始狀態
for(int i=2;i<=n;i++){
for(int j=1;j<=cnt[m];j++){
for(int k=0;k<(1<<m);k++){//列舉前i-1行的狀態
if((d[m][j]^k)&k)continue;//排除不補空的狀態
f[i][d[m][j]^k]+=f[i-1][k];
}
}
}
printf("%lld\n",f[n][0]);
}
return 0;
}