1. 程式人生 > 其它 >「POJ2411」Mondriaan's Dream 題解 (狀壓DP)

「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\ 0\ 0\ (12) \]

那麼第二排就可以這樣放:

\[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;
}

$$-----EOF-----$$