题解:洛谷 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
【核心思想】
-
问题分析:给定 \(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\)。 -
算法选择:
- 二维动态规划(逆序扫描):从右下角向左上角递推,利用"覆盖 \((i,j)\) 的操作必然也覆盖 \((i+1,j)\) 或 \((i,j+1)\) 的某个超集"这一单调性
- 操作编号压缩:不直接存储字符,而是存储每个格子最后被哪个操作覆盖(用操作编号 \(0\) 到 \(Q\) 表示,\(0\) 代表初始状态
A)
-
关键步骤:
- 初始化:创建二维数组 \(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]]\)(操作编号对应的字符)
- 初始化:创建二维数组 \(a[1..H][1..W]\),初始全为 \(0\)(表示初始状态
-
时间/空间复杂度:
- 时间复杂度:\(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]\)
-
二维 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
浙公网安备 33010602011771号