题目链接
题目描述
荷兰画家皮特·蒙德里安(Piet Mondrian)对矩形填充情有独钟。在一场梦中,他想用若干块 的小矩形(可以水平或竖直摆放)完全覆盖一块
的大矩形。
请你帮助蒙德计算:给定 和
,不同填充方案的数量。若无法完全覆盖,则答案为
。
需要注意的是,大矩形是定向的,即若两种方案通过翻转或旋转得到,但铺砌形状不同,也视为不同的方案。
解题思路
这是一个经典的棋盘覆盖计数问题,由于某一维的长度 通常较小(例如
),非常适合使用状态压缩动态规划(也常被称为轮廓线 DP)来解决。
核心思想
我们的核心思想是逐列填充这个 的矩形。当我们决定如何填充第
列时,我们唯一需要关心的“历史信息”是第
列的哪些格子有横向的
骨牌伸了过来,占用了第
列的位置。这个“历史信息”就是所谓的轮廓线状态。
我们可以用一个 位的二进制数来表示这个轮廓线状态,从而进行动态规划。
1. 初步判断与预处理
- 奇偶性判断: 一个
的骨牌覆盖两个格子。因此,要完全覆盖
的矩形,总面积
必须是偶数。如果
是奇数,则无法覆盖,方案数为
。
- 维度统一: 为了方便处理,我们可以规定
是较小的维度。由于
的矩形和
的矩形的填充方案数是一样的,若
,我们可以交换它们。
2. 状态定义
我们定义 表示:已经成功铺满前
列,且第
列向第
列伸出骨牌的状态为
时的方案数。
: 列的编号,从
到
。
: 一个
位的二进制整数。
的第
位为
,表示
处的格子放了一个横向骨牌,该骨牌占据了
的位置。
的第
位为
,表示
位置没有被上一列伸过来的骨牌占据。
3. 状态转移
我们的目标是根据 的所有值,计算出
的所有值。
对于一个给定的 ,它表示前
列已铺好,且对第
列的影响是
。现在我们需要铺第
列。
第 列的某些格子可能已经被
占了。我们需要用新的骨牌(横向或纵向)铺满第
列所有剩下的空格。铺设的结果会对第
列产生影响,这个影响就是
。
我们可以通过一个深度优先搜索(DFS)函数来枚举在给定 的情况下,所有合法的铺设第
列的方式,并计算出每种方式对应的
。
:
- 含义: 在处理第
列时,当前考虑到第
行,且已确定的对下一列的影响为
。
- 基准情形: 当
时,说明第
列的所有格子都已成功处理。我们找到了一种从
到
的有效过渡。于是,我们将方案数累加:
- 递归过程:
- 如果
已被
占据(即
的第
位为
),我们无法在此处放置新骨牌。直接进入下一行:
。
- 如果
未被占据:
- 选择1:放一个横向骨牌。这个骨牌会占据
和
。这会影响下一列,所以我们将
的第
位置为
。然后处理下一行:
。
- 选择2:放一个竖向骨牌。这个骨牌会占据
和
。这要求
也必须是空闲的(即
且
的第
位为
)。这个骨牌不影响下一列,所以
不变。由于一次性处理了两行,我们直接跳到第
行:
。
- 选择1:放一个横向骨牌。这个骨牌会占据
- 如果
4. 初始状态与最终答案
- 初始状态:
。这表示在开始铺第
列之前,我们已经“铺好”了前
列,对第
列的影响为
(即没有任何骨牌伸过来),这种情况只有一种(什么都不做)。
- 最终答案:
。这表示铺满了全部
列,并且没有骨牌伸出到不存在的第
列。
代码
#include <iostream>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
int m, n;
long long dp[12][1 << 12];
void dfs(int col, int row, int prev_mask, int current_mask) {
if (row == m) {
dp[col + 1][current_mask] += dp[col][prev_mask];
return;
}
// 如果当前单元格已被左侧的骨牌占据
if ((prev_mask >> row) & 1) {
dfs(col, row + 1, prev_mask, current_mask);
} else {
// 尝试放置一个 1x2 的横向骨牌
dfs(col, row + 1, prev_mask, current_mask | (1 << row));
// 尝试放置一个 2x1 的竖向骨牌
if (row + 1 < m && !((prev_mask >> (row + 1)) & 1)) {
dfs(col, row + 2, prev_mask, current_mask);
}
}
}
void solve() {
cin >> m >> n;
if (m > n) swap(m, n);
if ((m * n) % 2 != 0) {
cout << 0 << endl;
return;
}
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for (int i = 0; i < n; ++i) { // 遍历每一列
for (int j = 0; j < (1 << m); ++j) { // 遍历所有可能的上一列状态
if (dp[i][j] > 0) {
dfs(i, 0, j, 0);
}
}
}
cout << dp[n][0] << endl;
}
int main() {
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
import java.util.Scanner;
import java.util.Arrays;
public class Main {
private static int m, n;
private static long[][] dp;
private static void dfs(int col, int row, int prevMask, int currentMask) {
if (row == m) {
dp[col + 1][currentMask] += dp[col][prevMask];
return;
}
// 如果当前单元格已被左侧的骨牌占据
if (((prevMask >> row) & 1) == 1) {
dfs(col, row + 1, prevMask, currentMask);
} else {
// 尝试放置一个 1x2 的横向骨牌
dfs(col, row + 1, prevMask, currentMask | (1 << row));
// 尝试放置一个 2x1 的竖向骨牌
if (row + 1 < m && ((prevMask >> (row + 1)) & 1) == 0) {
dfs(col, row + 2, prevMask, currentMask);
}
}
}
public static void solve() {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
while (t-- > 0) {
m = sc.nextInt();
n = sc.nextInt();
if (m > n) {
int temp = m;
m = n;
n = temp;
}
if ((m * n) % 2 != 0) {
System.out.println(0);
continue;
}
dp = new long[n + 1][1 << m];
for (long[] row : dp) {
Arrays.fill(row, 0);
}
dp[0][0] = 1;
for (int i = 0; i < n; i++) { // 遍历每一列
for (int j = 0; j < (1 << m); j++) { // 遍历所有可能的上一列状态
if (dp[i][j] > 0) {
dfs(i, 0, j, 0);
}
}
}
System.out.println(dp[n][0]);
}
}
public static void main(String[] args) {
solve();
}
}
import sys
def solve():
m, n = map(int, sys.stdin.readline().split())
if m > n:
m, n = n, m
if (m * n) % 2 != 0:
print(0)
return
dp = [[0] * (1 << m) for _ in range(n + 1)]
dp[0][0] = 1
memo = {}
def get_transitions(prev_mask):
if prev_mask in memo:
return memo[prev_mask]
transitions = []
def dfs(row, current_mask):
if row == m:
transitions.append(current_mask)
return
if (prev_mask >> row) & 1:
dfs(row + 1, current_mask)
else:
# 放置横向骨牌
dfs(row + 1, current_mask | (1 << row))
# 放置竖向骨牌
if row + 1 < m and not ((prev_mask >> (row + 1)) & 1):
dfs(row + 2, current_mask)
dfs(0, 0)
memo[prev_mask] = transitions
return transitions
for i in range(n):
memo.clear()
for prev_mask in range(1 << m):
if dp[i][prev_mask] == 0:
continue
transitions = get_transitions(prev_mask)
for next_mask in transitions:
dp[i + 1][next_mask] += dp[i][prev_mask]
print(dp[n][0])
T = int(sys.stdin.readline())
for _ in range(T):
solve()
算法及复杂度
- 算法: 状态压缩动态规划。
- 时间复杂度:
, 其中
是填充一列的平均计算时间。通过 DFS 枚举一列的填充方式,其可能的路径数与斐波那契数增长类似,大致为
(
为黄金分割率)。因此,一个粗略的上限是
,但由于状态转移的稀疏性,实际运行速度远快于此,更接近
。
- 空间复杂度:
,用于存储 DP 状态表。可以通过滚动数组优化到
。