跳到主要内容

AcWing 291. 蒙德里安的梦想

本节目标

预处理合法轮廓转移,按列推进统计矩形铺法。

题意与约束

1×2 多米诺骨牌铺满 n×m 棋盘,输入有多组,0 0 停止,输出每组铺法数。

第一反应与重复子问题

逐块选择会重复面对相同的列边界;只记录当前列哪些格子已被上一列横放骨牌占用即可。

状态定义与转移推导

mask 表示当前列已占用位,递归填充本列生成 nextMaskdp[mask] 向每个合法 nextMask 累加。

正确性依据

列内从上到下填第一个空格:要么横放并在下一列留下占用位,要么与下一行竖放,完整且不重不漏。

样例执行过程

2×3 的空轮廓经过三列后回到空轮廓,共有 3 种铺法;10×11 的结果需以 64 位保存。

代码实现

C++17
#include <algorithm>
#include <functional>
#include <iostream>
#include <vector>
using namespace std;

long long countTilings(int n, int m) {
if (n > m) {
swap(n, m);
}
int states = 1 << n;
vector<vector<int>> transitions(states);
for (int mask = 0; mask < states; mask++) {
function<void(int, int)> fill = [&](int row, int nextMask) {
if (row == n) {
transitions[mask].push_back(nextMask);
return;
}
if (mask & (1 << row)) {
fill(row + 1, nextMask);
} else {
fill(row + 1, nextMask | (1 << row));
if (row + 1 < n && !(mask & (1 << (row + 1)))) {
fill(row + 2, nextMask);
}
}
};
fill(0, 0);
}
vector<long long> dp(states);
vector<long long> next(states);
dp[0] = 1;
for (int column = 0; column < m; column++) {
fill(next.begin(), next.end(), 0);
for (int mask = 0; mask < states; mask++) {
for (int nextMask : transitions[mask]) {
next[nextMask] += dp[mask];
}
}
dp.swap(next);
}
return dp[0];
}

#ifndef ALGORITHM_TUTORIAL_NO_MAIN
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
int m;
while (cin >> n >> m && (n || m)) {
cout << countTilings(n, m) << '\n';
}
}
#endif

复杂度分析

w = min(n, m)l = max(n, m),并记所有合法轮廓转移的数量为 T。预处理合法状态与转移需要 O(2^w · w + T) 时间,随后逐列转移需要 O(T · l) 时间;滚动数组占用 O(2^w) 空间,转移表占用 O(T) 空间。

边界与易错点

1×1 无法铺满;必须处理终止标记前所有棋盘并在 0 0 后停止,不能把结果放进 32 位整数。

模式迁移

棋盘局部关系只跨相邻列时可用轮廓 DP;本章不进一步扩展到插头 DP。回到状态压缩动态规划