题解:洛谷 AT_abc464_e Fill-Rect Query

【题目来源】

洛谷:AT_abc464_e [ABC464E] Fill-Rect Query - 洛谷

【题目描述】

There is an \(H \times W\) grid. Initially, every cell has A written on it. The cell at the \(i\)-th row from the top and \(j\)-th column from the left is denoted by \((i, j)\).

We will perform \(Q\) operations in order.
In the \(i\)-th operation, overwrite all cells in the rectangle with upper-left cell \((1, 1)\) and lower-right cell \((R_i, C_i)\) with the uppercase English letter \(X_i\).

Output the grid after all operations have been performed.

有一个 \(H \times W\) 的网格。初始时,每个格子上都写着 A。从上往下第 \(i\) 行、从左往右第 \(j\) 列的格子记为 \((i, j)\)

我们将按顺序执行 \(Q\) 次操作。
在第 \(i\) 次操作中,将左上角为 \((1, 1)\)、右下角为 \((R_i, C_i)\) 的矩形区域内的所有格子覆盖为大写英文字母 \(X_i\)

输出所有操作执行完毕后的网格。

【输入】

The input is given from Standard Input in the following format:

\(H\) \(W\) \(Q\)
\(R_1\) \(C_1\) \(X_1\)
\(R_2\) \(C_2\) \(X_2\)
\(\vdots\)
\(R_Q\) \(C_Q\) \(X_Q\)

【输出】

Print \(H\) lines. The \(i\)-th line should contain a string of length \(W\), whose \(j\)-th character is the uppercase English letter written on \((i, j)\) in the grid after the operations.

【输入样例】

2 3 3
2 2 B
1 3 C
2 1 D

【输出样例】

DCC
DBA

【核心思想】

  1. 问题分析:给定 \(H \times W\) 的网格,初始全为 A。执行 \(Q\) 次操作,每次将左上角 \((1,1)\) 到右下角 \((R_i, C_i)\) 的矩形覆盖为字符 \(X_i\)。由于操作按顺序执行且后覆盖的会覆盖先覆盖的,格子 \((i,j)\) 的最终字符由覆盖该格子的操作中编号最大的那个决定。关键观察是:操作 \(k\) 覆盖格子 \((i,j)\) 当且仅当 \(R_k \geq i\)\(C_k \geq j\)

  2. 算法选择

    • 二维动态规划(逆序扫描):从右下角向左上角递推,利用"覆盖 \((i,j)\) 的操作必然也覆盖 \((i+1,j)\)\((i,j+1)\) 的某个超集"这一单调性
    • 操作编号压缩:不直接存储字符,而是存储每个格子最后被哪个操作覆盖(用操作编号 \(0\)\(Q\) 表示,\(0\) 代表初始状态 A
  3. 关键步骤

    • 初始化:创建二维数组 \(a[1..H][1..W]\),初始全为 \(0\)(表示初始状态 A
    • 记录操作位置:对于第 \(i\) 次操作 \((R_i, C_i, X_i)\),在 \(a[R_i][C_i]\) 处记录操作编号 \(i\)(该点是该矩形覆盖范围的"右下角标记")
    • 逆序 DP 传播
      • \(i = H\)\(1\)\(j = W\)\(1\) 遍历
      • \(a[i][j] = \max(a[i][j], a[i+1][j], a[i][j+1])\)
      • 含义:格子 \((i,j)\) 的最终操作编号 = 自身被直接标记的操作、下方格子继承的操作、右方格子继承的操作中编号最大的
    • 输出结果:对每个格子 \((i,j)\),输出 \(x[a[i][j]]\)(操作编号对应的字符)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(H \times W + Q)\),记录操作 \(O(Q)\),逆序 DP 遍历网格 \(O(H \times W)\)
    • 空间复杂度:\(O(H \times W + Q)\),存储操作数组 \(x[0..Q]\) 和二维 DP 数组 \(a[1..H][1..W]\)
  5. 二维 DP 逆序传播的核心思想

    • 单调性利用:操作 \((1,1)\)\((R,C)\) 的矩形覆盖具有"右下包含"特性——若操作覆盖 \((i,j)\),则其右下角 \((R,C)\) 满足 \(R \geq i\)\(C \geq j\)。因此从右下角向左上角递推时,每个格子只需考虑自身、下方、右方三个来源
    • 操作编号即优先级:用操作编号代替字符,天然利用编号递增表示时间先后顺序,取最大值等价于取"最后执行的操作"
    • 空间压缩技巧:不存储整个矩形的覆盖信息,仅在矩形的右下角记录一个"标记点",通过 DP 传播将该标记扩散到整个矩形区域
    • 边界处理:数组多开一圈边界(\(H+2\)\(W+2\)),避免 \(a[i+1][j]\)\(a[i][j+1]\) 越界,简化代码逻辑
    • 适用于多次矩形覆盖且覆盖区域具有固定左上角(或固定模式)的网格更新问题,将 \(O(Q \cdot H \cdot W)\) 的暴力模拟优化至 \(O(H \cdot W)\)

【算法标签】

普及 #线性DP-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 1000005, Q = 200005;  // N: 未使用, Q: 最大操作数
int h, w, q;                        // h: 网格高度, w: 网格宽度, q: 操作数
char x[Q];                          // x[i]: 第i次操作覆盖的字符(x[0]='A'表示初始状态)

int main()
{
    cin >> h >> w >> q;             // 读入网格高度、宽度和操作数

    x[0] = 'A';                     // 初始状态所有格子都是'A'

    // a[i][j]: 格子(i,j)最后被覆盖的操作编号(0表示未被任何操作覆盖,即初始状态'A')
    // 用二维vector初始化,大小为(h+2)*(w+2),多留一圈边界避免越界
    vector<vector<int>> a(h + 2, vector<int>(w + 2, 0));

    // 读入q次操作
    for (int i = 1; i <= q; i++)
    {
        int r, c;                   // r: 矩形右下角行号, c: 矩形右下角列号
        cin >> r >> c >> x[i];      // 读入操作参数和覆盖字符

        // 记录格子(r,c)的操作编号
        // 注意:这里只记录了右下角一个点,而非整个矩形
        // 后续通过DP从右下角向左上角传播,确定每个格子的最终操作
        a[r][c] = i;
    }

    // ========== 核心DP:从右下角向左上角传播操作编号 ==========
    // 原理:每个格子(i,j)的最终操作,是其自身记录的操作、下方格子的操作、右方格子操作中编号最大的
    // 因为操作按顺序执行,编号大的操作后执行,会覆盖编号小的操作
    // 对于矩形覆盖(1,1)到(r,c),格子(i,j)被覆盖当且仅当存在某个操作满足 r>=i 且 c>=j
    // 从右下角往左上角遍历,a[i][j] 表示覆盖格子(i,j)的最后操作编号

    for (int i = h; i >= 1; i--)
    {
        for (int j = w; j >= 1; j--)
        {
            // 当前格子的最终操作 = max(自身记录的操作, 下方格子的操作, 右方格子的操作)
            // 下方和右方的格子已经被处理过,包含了更右下角的信息
            a[i][j] = max({a[i][j], a[i + 1][j], a[i][j + 1]});
        }
    }

    // 输出最终网格
    for (int i = 1; i <= h; i++)
    {
        for (int j = 1; j <= w; j++)
        {
            // a[i][j]是覆盖格子(i,j)的最后操作编号
            // x[a[i][j]]对应该操作的覆盖字符
            cout << x[a[i][j]];
        }
        cout << endl;
    }

    return 0;
}

【运行结果】

2 3 3
2 2 B
1 3 C
2 1 D
DCC
DBA
posted @ 2026-06-30 21:15  团爸讲算法  阅读(15)  评论(0)    收藏  举报