前言
多米諾骨牌的棋盤覆蓋問題是一類經(jīng)典的數(shù)學(xué)/算法問題。本文討論添加了免分割線約束條件的多米諾骨牌覆蓋問題。
問題描述
多米諾骨牌是 2 × 1 或 1 × 2 的矩形。
考慮用多米諾骨牌覆蓋 m × n棋盤,如果無法通過一條不穿過任何骨牌內(nèi)部的直線,將一種覆蓋方案分割成兩個部分,那么這種覆蓋方案被稱為是穩(wěn)定的。

存在性問題:對于 m × n 的棋盤,是否存在穩(wěn)定的多米諾骨牌的覆蓋方案?
計數(shù)問題:對于 m × n 的棋盤,有多少種穩(wěn)定的多米諾骨牌的覆蓋方案?
輸入輸出
Input
多組測試數(shù)據(jù)。每組測試數(shù)據(jù)的第一行包含兩個整數(shù) m 和 n (1 ≤ m, n ≤ 16) 表示矩形的大小。
Output
對于每組測試數(shù)據(jù),輸出一行一個整數(shù)表示對應(yīng)穩(wěn)定多米諾骨牌覆蓋的方案數(shù)。
答案可能很大,輸出模 1000000007 (10^9 + 7) 的結(jié)果。
Input示例
2 2
5 6
8 7
15 16
Output示例
0
6
13514
856463275
存在性問題
存在性問題在這文章里已經(jīng)解釋得很清楚。在這里只是給出該文章的一些思路和線索。
首先m,n不能都為奇數(shù)。因為當(dāng)兩者都為奇數(shù)時,棋盤格子總數(shù)m × n 為奇數(shù),而骨牌覆蓋格子數(shù)為偶數(shù),所以不存在覆蓋方案。
下面進一步討論m,n的可能取值情況,并假設(shè)m,n不同時為奇數(shù)。根據(jù)對稱性,不妨假設(shè)m≤ n。我們自小到大討論m的可能取值。
- m=1或m=2。顯然只有平凡解 (m,n) = (1, 2) 。
- m=3或m=4。自左往右分析可能的放置方式。根據(jù)免分割線條件,只能放置成幾種固定模式,且這幾種模式都不能構(gòu)成齊整的右邊緣。
- (m,n) = (5,6), (6,8) 時可以構(gòu)造出解。我們將這兩個解看作(m,n) = (奇數(shù) ,偶數(shù)), (偶數(shù),偶數(shù))的基礎(chǔ)解。基于這兩個基礎(chǔ)解,我們能通過擴展方法構(gòu)造出其他(奇數(shù) ,偶數(shù)), (偶數(shù),偶數(shù))情況下的解。擴展方法是將覆蓋最后一列/行的骨牌平移兩個格子,并在平移產(chǎn)生的空隙中插入橫/豎放的骨牌。
- (m,n) = (6,6) 時無解。無解的證明方法是反證法+“算兩次”。假設(shè)結(jié)論成立。覆蓋骨牌數(shù)是18。注意到,穿過每條分割線的骨牌數(shù)必須是偶數(shù),否則由該分割線分割的兩部分中的任意份都只有奇數(shù)格子,無法被覆蓋,所以每條分割線至少穿過2個骨牌。另外分割線共有10條,因此總骨牌數(shù)至少為20個。矛盾!
綜上,得出除平凡解外的解的充分必要條件:
- M 和 N 至少有一個是偶數(shù)。
- M 和 N 都大于 4 。
- M 和 N 不同時等于 6 。
計數(shù)問題
棋盤類的計數(shù)問題很容易想到用動態(tài)規(guī)劃求解。但是這里存在一個較大的問題,分割線在水平方向和豎直方向上都不能有分割線,這個性質(zhì)使得我們很難將覆蓋問題切分為更小的子問題求解,也即是說很難直接應(yīng)用動態(tài)規(guī)劃。對于這一類含有全局性質(zhì)的計數(shù)問題,可以考慮用容斥原理計算。將求解具有穩(wěn)定性質(zhì)的覆蓋問題轉(zhuǎn)化成求解無穩(wěn)定性質(zhì)的覆蓋問題。
不考慮分割線覆蓋方案稱為完美覆蓋。完美覆蓋的計數(shù)問題可以使用動態(tài)規(guī)劃求解。
假設(shè)已經(jīng)求出了 i × j 棋盤的完美覆蓋數(shù)(Pij)。我們對列運用容斥原理。容斥原理中的Ai表示被i條豎直分割線分割的無橫向分割線的覆蓋方案;S表示所有無橫向分割線的覆蓋方案。為了敘述方便,我們稱若干個Ai的交稱為豎直分割方案。根據(jù)容斥原理,我們需要計算每種豎直分割方案下的無橫向分割線的覆蓋數(shù)(R)。我們使用按行(r)遞推的方式求解R, 其中Ri表示 i × n棋盤的無橫向分割線的覆蓋數(shù), 所以R=Rm。為了求解Ri,我們定義一個豎直分割方案下 i × n棋盤的完美覆蓋數(shù)為SPi,SPi的值等于被豎直分割線切分的所有小棋盤完美覆蓋數(shù)的乘積。假設(shè)豎直方案將棋盤切分為3個小棋盤( i × a, i × b, i × c),其中a+b+c=n。則SPi = Pia × Pib × Pic. 根據(jù)上述定義,Ri的遞推公式如下:
- r = 1, R1 = SP1
- r = k, Rk = SPk - R1 × SPk-1 - R2 × SPk-2 - … - Rk-1 × SP1
遞推原理是將SPk按照位置最高的橫向分割線(rc)將覆蓋方案分為不相交的k類。每條rc將棋盤分為上下兩部分,上部分無橫向分割線,下部分允許有橫向分割線。假設(shè)上部分有l(wèi)行,則該類的覆蓋方案數(shù)為Rl × SPk-l
代碼如下(含完美覆蓋):
#include<iostream>
using namespace std;
#define LL long long
#define N 16
LL P[N + 1][N + 1];
LL pc[N + 1][N + 1][1 << N];
LL mod = 1000000007;
pair<LL, LL> p(LL x, LL y, LL i, LL col) {
if (y > i) return make_pair(x, y - i);
else return make_pair(x - 1, col);
}
void initP(LL col) {
LL stateNum = 1 << col;
for (LL j = 1; j <= col; j++) {
pc[0][j][stateNum - 1] = 1;
}
for (LL i = 1; i <= N; i++) {
for (LL j = 1; j <= col; j++) {
LL b = 1 << (j - 1);
for (LL k = 0; k < stateNum; k++) {
auto pre = p(i, j, 1, col);
pc[i][j][k] = pc[pre.first][pre.second][k ^ b];
LL b2 = 3 << (j - 2);
if (j > 1 && (b2 & k) == b2) {
auto pre1 = p(i, j, 2, col);
pc[i][j][k] = (pc[i][j][k] + pc[pre1.first][pre1.second][k]) % mod;
}
}
}
P[i][col] = pc[i][col][stateNum - 1];
}
}
void initP() {
for (LL j = 1; j <= N; j++) {
initP(j);
}
}
void getSection(LL state, LL sec[], LL &l, LL y) {
LL start = 0;
for (LL j = 1; j < y; j++) {
if (state & (1 << (j - 1))) {
sec[l++] = j - start;
start = j;
}
}
sec[l++] = y - start;
}
int main() {
initP();
LL x, y;
while (cin >> x >> y) {
LL stateNum = 1 << (y - 1);
LL ans = 0;
for (LL i = 0; i < stateNum; i++) {
LL section[N];
LL sectionLength = 0;
LL R[N + 1];
LL SP[N + 1];
getSection(i, section, sectionLength, y);
for (LL r = 1; r <= x; r++) {
SP[r] = 1;
for (LL j = 0; j < sectionLength; j++)
SP[r] = SP[r] * P[r][section[j]] % mod;
}
for (LL r = 1; r <= x; r++) {
R[r] = SP[r];
for (LL l = 1; l < r; l++) {
R[r] = (R[r] - R[l] * SP[r - l]) % mod;
}
}
if (sectionLength % 2 == 0)
ans = (ans - R[x]) % mod;
else
ans = (ans + R[x]) % mod;
}
cout << (ans + mod) % mod << endl;
}
return 0;
}