前言
多米诺骨牌的棋盘覆盖问题是一类经典的数学/算法问题。本文讨论添加了免分割线约束条件的多米诺骨牌覆盖问题。
问题描述
多米诺骨牌是 2 × 1 或 1 × 2 的矩形。
考虑用多米诺骨牌覆盖 m × n棋盘,如果无法通过一条不穿过任何骨牌内部的直线,将一种覆盖方案分割成两个部分,那么这种覆盖方案被称为是稳定的。
存在性问题:对于 m × n 的棋盘,是否存在稳定的多米诺骨牌的覆盖方案?
计数问题:对于 m × n 的棋盘,有多少种稳定的多米诺骨牌的覆盖方案?
输入输出
Input
多组测试数据。每组测试数据的第一行包含两个整数 m 和 n (1 ≤ m, n ≤ 16) 表示矩形的大小。
Output
对于每组测试数据,输出一行一个整数表示对应稳定多米诺骨牌覆盖的方案数。
答案可能很大,输出模 1000000007 (10^9 + 7) 的结果。
Input示例
2 2
5 6
8 7
15 16
Output示例
0
6
13514
856463275
存在性问题
存在性问题在这文章里已经解释得很清楚。在这里只是给出该文章的一些思路和线索。
首先m,n不能都为奇数。因为当两者都为奇数时,棋盘格子总数m × n 为奇数,而骨牌覆盖格子数为偶数,所以不存在覆盖方案。
下面进一步讨论m,n的可能取值情况,并假设m,n不同时为奇数。根据对称性,不妨假设m≤ n。我们自小到大讨论m的可能取值。
- m=1或m=2。显然只有平凡解 (m,n) = (1, 2) 。
- m=3或m=4。自左往右分析可能的放置方式。根据免分割线条件,只能放置成几种固定模式,且这几种模式都不能构成齐整的右边缘。
- (m,n) = (5,6), (6,8) 时可以构造出解。我们将这两个解看作(m,n) = (奇数 ,偶数), (偶数,偶数)的基础解。基于这两个基础解,我们能通过扩展方法构造出其他(奇数 ,偶数), (偶数,偶数)情况下的解。扩展方法是将覆盖最后一列/行的骨牌平移两个格子,并在平移产生的空隙中插入横/竖放的骨牌。
- (m,n) = (6,6) 时无解。无解的证明方法是反证法+“算两次”。假设结论成立。覆盖骨牌数是18。注意到,穿过每条分割线的骨牌数必须是偶数,否则由该分割线分割的两部分中的任意份都只有奇数格子,无法被覆盖,所以每条分割线至少穿过2个骨牌。另外分割线共有10条,因此总骨牌数至少为20个。矛盾!
综上,得出除平凡解外的解的充分必要条件:
- M 和 N 至少有一个是偶数。
- M 和 N 都大于 4 。
- M 和 N 不同时等于 6 。
计数问题
棋盘类的计数问题很容易想到用动态规划求解。但是这里存在一个较大的问题,分割线在水平方向和竖直方向上都不能有分割线,这个性质使得我们很难将覆盖问题切分为更小的子问题求解,也即是说很难直接应用动态规划。对于这一类含有全局性质的计数问题,可以考虑用容斥原理计算。将求解具有稳定性质的覆盖问题转化成求解无稳定性质的覆盖问题。
不考虑分割线覆盖方案称为完美覆盖。完美覆盖的计数问题可以使用动态规划求解。
假设已经求出了 i × j 棋盘的完美覆盖数(Pij)。我们对列运用容斥原理。容斥原理中的Ai表示被i条竖直分割线分割的无横向分割线的覆盖方案;S表示所有无横向分割线的覆盖方案。为了叙述方便,我们称若干个Ai的交称为竖直分割方案。根据容斥原理,我们需要计算每种竖直分割方案下的无横向分割线的覆盖数(R)。我们使用按行(r)递推的方式求解R, 其中Ri表示 i × n棋盘的无横向分割线的覆盖数, 所以R=Rm。为了求解Ri,我们定义一个竖直分割方案下 i × n棋盘的完美覆盖数为SPi,SPi的值等于被竖直分割线切分的所有小棋盘完美覆盖数的乘积。假设竖直方案将棋盘切分为3个小棋盘( i × a, i × b, i × c),其中a+b+c=n。则SPi = Pia × Pib × Pic. 根据上述定义,Ri的递推公式如下:
- r = 1, R1 = SP1
- r = k, Rk = SPk - R1 × SPk-1 - R2 × SPk-2 - … - Rk-1 × SP1
递推原理是将SPk按照位置最高的横向分割线(rc)将覆盖方案分为不相交的k类。每条rc将棋盘分为上下两部分,上部分无横向分割线,下部分允许有横向分割线。假设上部分有l行,则该类的覆盖方案数为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;
}