题目链接

PEEK147 蒙德的梦

题目描述

荷兰画家皮特·蒙德里安(Piet Mondrian)对矩形填充情有独钟。在一场梦中,他想用若干块 的小矩形(可以水平或竖直摆放)完全覆盖一块 的大矩形。

请你帮助蒙德计算:给定 ,不同填充方案的数量。若无法完全覆盖,则答案为

需要注意的是,大矩形是定向的,即若两种方案通过翻转或旋转得到,但铺砌形状不同,也视为不同的方案。

解题思路

这是一个经典的棋盘覆盖计数问题,由于某一维的长度 通常较小(例如 ),非常适合使用状态压缩动态规划(也常被称为轮廓线 DP)来解决。

核心思想

我们的核心思想是逐列填充这个 的矩形。当我们决定如何填充第 列时,我们唯一需要关心的“历史信息”是第 列的哪些格子有横向的 骨牌伸了过来,占用了第 列的位置。这个“历史信息”就是所谓的轮廓线状态

我们可以用一个 位的二进制数来表示这个轮廓线状态,从而进行动态规划。

1. 初步判断与预处理

  • 奇偶性判断: 一个 的骨牌覆盖两个格子。因此,要完全覆盖 的矩形,总面积 必须是偶数。如果 是奇数,则无法覆盖,方案数为
  • 维度统一: 为了方便处理,我们可以规定 是较小的维度。由于 的矩形和 的矩形的填充方案数是一样的,若 ,我们可以交换它们。

2. 状态定义

我们定义 表示:已经成功铺满前 列,且第 列向第 列伸出骨牌的状态为 时的方案数。

  • : 列的编号,从
  • : 一个 位的二进制整数。 的第 位为 ,表示 处的格子放了一个横向骨牌,该骨牌占据了 的位置。 的第 位为 ,表示 位置没有被上一列伸过来的骨牌占据。

3. 状态转移

我们的目标是根据 的所有值,计算出 的所有值。

对于一个给定的 ,它表示前 列已铺好,且对第 列的影响是 。现在我们需要铺第 列。

列的某些格子可能已经被 占了。我们需要用新的骨牌(横向或纵向)铺满第 列所有剩下的空格。铺设的结果会对第 列产生影响,这个影响就是

我们可以通过一个深度优先搜索(DFS)函数来枚举在给定 的情况下,所有合法的铺设第 列的方式,并计算出每种方式对应的

:

  • 含义: 在处理第 列时,当前考虑到第 行,且已确定的对下一列的影响为
  • 基准情形: 当 时,说明第 列的所有格子都已成功处理。我们找到了一种从 的有效过渡。于是,我们将方案数累加:
  • 递归过程:
    1. 如果 已被 占据(即 的第 位为 ),我们无法在此处放置新骨牌。直接进入下一行:
    2. 如果 未被占据:
      • 选择1:放一个横向骨牌。这个骨牌会占据 。这会影响下一列,所以我们将 的第 位置为 。然后处理下一行:
      • 选择2:放一个竖向骨牌。这个骨牌会占据 。这要求 也必须是空闲的(即 的第 位为 )。这个骨牌不影响下一列,所以 不变。由于一次性处理了两行,我们直接跳到第 行:

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 状态表。可以通过滚动数组优化到