Skip to content

Domino - 题解

标签与难度

标签: 构造, 势能分析, 前缀和, 网格图, 思维题 难度: 2100

题目大意喵~

你好呀,指挥官!这道题是关于多米诺骨牌的喵~

我们有一个 n×mn \times m 的大棋盘,它被 1×21 \times 2 (横向) 和 2×12 \times 1 (纵向) 的多米诺骨牌完美地铺满了。我们可以对这个棋盘做两种神奇的操作:

  1. 在一个 2×22 \times 2 的小方格里,如果它正好被两个横向的骨牌覆盖,我们可以把它们旋转一下,变成两个纵向的骨牌。
  2. 同样,在一个 2×22 \times 2 的小方格里,如果它被两个纵向的骨牌覆盖,我们也可以把它们旋转成两个横向的骨牌。

就像这样,喵~:

--      ||
--  <=> ||

现在我们有两份铺好的 n×mn \times m 棋盘,初始状态(A)和目标状态(B)。问题是,我们最少需要多少次这样的旋转操作,才能把初始状态的棋盘变得和目标状态一模一样呢?

解题思路分析

这道题看起来有点棘手,因为它涉及状态转换和最少操作次数,可能会让人想到复杂的搜索算法,但其实有更巧妙的解法哦,喵~ 让我们一起用猫咪的直觉来破解它吧!

首先,把问题简化一下。将棋盘 A 变成棋盘 B,等价于将一个“差异”棋盘(A 相对于 B 的不同之处)变成一个“全同”棋盘(没有差异)。每一次操作,我们都是在改变局部骨牌的布局。我们的目标是找到一个衡量“差异”的量,并且这个量在每次操作后都会有非常规律的变化。这种方法,在算法竞赛中叫做势能分析,是不是听起来很酷?

让我们来构造一个“势能函数”吧!

1. 关注点分离

一个操作会同时影响横向和纵向的骨牌数量。为了简化问题,我们只关注一种骨牌,比如说,横向的。一个操作可以看作是:在某个 2×22 \times 2 区域内,要么减少两个横向骨牌(同时增加两个纵向骨牌),要么增加两个横向骨牌(同时减少两个纵向骨牌)。

2. 定义差异场

我们创建一个新的 n×mn \times m 的网格,叫做 diff 好了。对于每个单元格 (i, j),我们记录两个棋盘在这里横向骨牌的差异。

  • h_A(i, j): 如果棋盘 A 在 (i, j) 位置有一个横向骨牌的左半部分,则为 1,否则为 0。
  • h_B(i, j): 如果棋盘 B 在 (i, j) 位置有一个横向骨牌的左半部分,则为 1,否则为 0。
  • diff[i][j] = h_A(i, j) - h_B(i, j)。这个值只可能是 -1, 0, 1。

现在,我们的目标就是通过操作,把整个 diff 网格变成全 0。

3. 寻找“势”

一个在 (r, c)(2×22 \times 2 方格的左上角)的操作,会影响到 (r, c) 和 (r+1, c) 两处的横向骨牌。

  • 如果是 横 -> 竖,那么 h_A(r, c)h_A(r+1, c) 都从 1 变为 0。diff[r][c]diff[r+1][c] 都减少了 1。
  • 如果是 竖 -> 横,那么 h_A(r, c)h_A(r+1, c) 都从 0 变为 1。diff[r][c]diff[r+1][c] 都增加了 1。

每次操作影响了两个位置,还是有点麻烦。能不能让它只影响一个位置呢?

这里就是魔法发生的地方啦!我们引入一个交替的符号。让我们定义一个新的势能场 potential_diff

potential_diff[i][j]=(hA(i,j)hB(i,j))×(1)i\text{potential\_diff}[i][j] = (h_A(i, j) - h_B(i, j)) \times (-1)^i

现在再看看一次操作的影响。假设在 (r, c) 进行一次 横 -> 竖 的操作:

  • potential_diff[r][c] 的变化量是: 1×(1)r-1 \times (-1)^r
  • potential_diff[r+1][c] 的变化量是: 1×(1)r+1-1 \times (-1)^{r+1}

注意到 (1)r+1=(1)r(-1)^{r+1} = -(-1)^r 了吗?喵!这意味着,如果 potential_diff[r][c] 减少了 v,那么 potential_diff[r+1][c] 就会减少 -v,也就是增加了 v!一个位置减少,另一个位置就增加相同的量。

4. 前缀和的魔法

一个加一个减,这让我们想到了什么?对啦,是前缀和

我们来构造最终的势能场 P,它是 potential_diff 在列方向上的前缀和:

P[i][j]=k=0ipotential_diff[k][j]P[i][j] = \sum_{k=0}^{i} \text{potential\_diff}[k][j]

现在,让我们再来看看在 (r, c) 进行一次操作对 P 的影响:

  • 对于任意 k < rP[k][j] 不变,因为 potential_diff 在这些行都没有变化。
  • 对于 k = rP[r][j] 的变化量是 potential_diff[r][c] 的变化量,即 ±1×(1)r\pm 1 \times (-1)^r。我们称之为 v。
  • 对于任意 k > rP[k][j] 的变化量是 potential_diff[r][c] 的变化量加上 potential_diff[r+1][c] 的变化量,也就是 v + (-v) = 0

太神奇了,喵!一次操作,只会让势能场 P 中的一个元素 P[r][c] 的值改变 +1 或者 -1

5. 计算最终答案

我们的目标是让两个棋盘相同,这等价于让所有的 diff[i][j] 都为 0,从而所有的 potential_diff[i][j]P[i][j] 也都为 0。

如果 P[r][c] 的当前值是 X,那么为了把它变成 0,我们需要进行 |X| 次能让 P[r][c] 的绝对值减 1 的操作。因为每次操作只影响一个 P 值,并且是 +/- 1 的变化,所以不同位置的操作是相互独立的。

所以,总的最少操作次数就是把所有 P[i][j] 都变成 0 所需操作次数的总和,也就是:

Total Operations=i=0n1j=0m1P[i][j]\text{Total Operations} = \sum_{i=0}^{n-1} \sum_{j=0}^{m-1} |P[i][j]|

总结一下我们的算法步骤:

  1. 创建一个 n×mn \times mpotential_diff 数组,全部初始化为 0。
  2. 遍历初始棋盘 A,如果 (i, j) 是一个横向骨牌的左半部分,就给 potential_diff[i][j] 加上 (i % 2 == 0 ? 1 : -1)
  3. 遍历目标棋盘 B,如果 (i, j) 是一个横向骨牌的左半部分,就从 potential_diff[i][j] 减去 (i % 2 == 0 ? 1 : -1)
  4. potential_diff 数组的每一列做前缀和,得到最终的势能场 P
  5. P 数组中所有元素的绝对值相加,就是我们想要的答案啦!

代码实现

这是我根据上面的思路,精心为你准备的代码哦~ 喵~

cpp
#include <iostream>
#include <vector>
#include <string>
#include <cmath>

// 使用一个函数来处理输入和计算,让主函数更整洁喵~
void solve() {
    int n, m;
    // 持续读取输入直到文件末尾
    while (std::cin >> n >> m) {
        // 为了方便处理,我们多留一些空间,避免边界检查的麻烦
        std::vector<std::vector<int>> potential_diff(n + 1, std::vector<int>(m + 1, 0));
        std::vector<std::string> grid(n);

        // --- 步骤 1 & 2: 处理初始棋盘 A ---
        for (int i = 0; i < n; ++i) {
            std::cin >> grid[i];
            for (int j = 0; j < m; ++j) {
                // 我们只关心横向骨牌的左半部分
                if (grid[i][j] == '-') {
                    // 根据行号的奇偶性给予不同的符号
                    // (i % 2 == 0 ? 1 : -1) 和 ( (i & 1) ? -1 : 1 ) 是一样的哦
                    int sign = (i % 2 == 0) ? 1 : -1;
                    potential_diff[i][j] += sign;
                    // 跳过横向骨牌的右半部分,防止重复计算
                    j++; 
                }
            }
        }

        // --- 步骤 3: 处理目标棋盘 B ---
        for (int i = 0; i < n; ++i) {
            std::cin >> grid[i];
            for (int j = 0; j < m; ++j) {
                if (grid[i][j] == '-') {
                    int sign = (i % 2 == 0) ? 1 : -1;
                    potential_diff[i][j] -= sign; // 从差异中减去
                    j++;
                }
            }
        }

        // --- 步骤 4: 计算列方向的前缀和 ---
        // 这会把 potential_diff 数组变成我们最终的势能场 P
        for (int j = 0; j < m; ++j) {
            for (int i = 1; i < n; ++i) {
                potential_diff[i][j] += potential_diff[i - 1][j];
            }
        }

        // --- 步骤 5: 累加势能场所有值的绝对值 ---
        long long total_ops = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                total_ops += std::abs(potential_diff[i][j]);
            }
        }

        std::cout << total_ops << std::endl;
    }
}

int main() {
    // 加速一下输入输出,让程序跑得更快,像猫咪一样敏捷!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    solve();
    return 0;
}

复杂度分析

  • 时间复杂度: O(N×M)O(N \times M) 我们对两个 N×MN \times M 的网格进行了几次完整的遍历:读取输入、构建 potential_diff 数组、计算前缀和、以及最后求和。每一步都是线性的,所以总时间复杂度是 O(N×M)O(N \times M),非常高效的说!

  • 空间复杂度: O(N×M)O(N \times M) 我们主要使用了一个 N×MN \times M 大小的 potential_diff 数组来存储我们的势能场,以及一个 grid 数组来读取输入。所以额外空间是 O(N×M)O(N \times M)

知识点总结

这道题真是一次有趣的思维探险,对吧?我们可以从中学习到:

  1. 势能分析 (Potential Function Method): 这是解决这类状态转换问题的强大武器。通过构造一个巧妙的“势能”函数,使得每次操作对势能的影响变得极其简单和局部化,从而将复杂问题转化为简单的计数问题。
  2. 前缀和 (Prefix Sum): 前缀和不仅能用于快速求区间和,还能像这道题一样,作为一种变换工具,将成对的、符号相反的变化(一个 +v,一个 -v)转化为单点变化,这是解题的关键一步。
  3. 构造性思维 (Constructive Thinking): 面对没有明显模板可以套用的问题时,深入分析操作的性质,尝试构造出能反映问题本质的数学模型,是通往解法的重要途径。

希望这篇题解能帮到你,指挥官!如果还有其他问题,随时可以来找我玩哦,喵~