专题6 · 二维前缀和与差分
把一维的「多退少补」推到平面上:
二维前缀和用容斥算出任意子矩阵的和,二维差分只改四个角就能刷完一整块矩形。
本讲用「地毯覆盖」与「激光炸弹」两道真题落地。
为什么要推广到二维
一维做的是「一段区间」,二维要做的就是「一块矩形」——思路完全一样,只是加减的地方多了几处。
📏 一维
前缀和:s[i] = s[i-1] + a[i]
区间和:s[r] - s[l-1](减 1 处)
区间加:b[l]+=x; b[r+1]-=x;(改 2 处)
🗺️ 二维
前缀和:S[i][j] = S[i-1][j] + S[i][j-1] - S[i-1][j-1] + a[i][j]
子矩阵和:加减 4 项(容斥)
矩形加:只改 4 个角
S[i][j] 是以 (i,j) 为右下角的矩形之和
📐 构建公式(背)
S[i][j] = S[i-1][j] + S[i][j-1]
- S[i-1][j-1] + a[i][j];
上方 + 左方 − 重叠的左上方(被算了两次)+ 自己。
这一步就是最朴素的容斥。
🧠 为什么减去 S[i-1][j-1]
S[i-1][j] 与 S[i][j-1] 两块重叠的部分正是 S[i-1][j-1],它被加了两次,所以要减掉一次。
记忆法:「上加、左加、左上减、自己加」。
S[0][*] 天然为 0,不用写任何边界特判。求 (x1,y1) 到 (x2,y2) 的子矩阵和
sum = S[x2][y2] // 整块 - S[x1-1][y2] // 减去上方多余 - S[x2][y1-1] // 减去左方多余 + S[x1-1][y1-1]; // 左上角被减两次,加回来
① 先取大块
从 (1,1) 到 (x2,y2) 的整块,肯定多算了目标以外的地方。
② 减上、减左
分别减去目标上方与左方的两块。
③ 补回左上角
左上角那一小块被减了两次,必须 +1 次补回来。
+ S[x1-1][y1-1] 忘了写,或写成减号。口诀:「减两加一」——减上、减左、加左上。S[1][1] - S[0][1] - S[1][0] + S[0][0] = S[1][1] = a[1][1] ✓。如果你记的版本算不出 a[1][1],那就记错了。二维前缀和 · 逐步构建与查询
矩形区域加 1,只需要改 4 个角
要给 (x1,y1) 到 (x2,y2) 的整块矩形加 x:
d[x1] [y1] += x; // 从左上角起「抬升」 d[x2+1] [y1] -= x; // 过了下边界,横向抵消 d[x1] [y2+1] -= x; // 过了右边界,纵向抵消 d[x2+1] [y2+1] += x; // 右下角被抵消两次,加回来
🧠 和一维的对应关系
一维:b[l]+=x; b[r+1]-=x;(2 个点)。
二维:把「点」换成「角」,2×2 = 4 个角,符号同样遵循容斥。
⚡ 复杂度
单次矩形修改 O(1),不管矩形多大。
最后统一做一次二维前缀和还原,O(n²)。
m 次修改总计 O(m + n²),而暴力是 O(m·n²)。
d[x2+1][y2+1],而 x2、y2 最大为 n,所以至少开 (n+2) × (n+2)。这是二维差分最常见的越界点。地毯覆盖:二维差分的模板题
n×n 网格,m 张地毯,每张覆盖一个矩形。问每个格子被覆盖几次。
🔍 建模
每张地毯 = 一次矩形 +1。
这正是二维差分唯一的用途场景:大量矩形修改,最后统一查询。
📋 三步流水线
- ① 读入每张地毯,改 d 的 4 个角。
- ② 对 d 做二维前缀和,还原成覆盖次数。
- ③ 按 n 行 n 列输出。
地毯覆盖 · 代码走读
const int N=1005; int d[N][N]; // n≤1000,要多开两格 int main(){ int n,m; cin>>n>>m; for(int i=0;i<m;i++){ int x1,y1,x2,y2; cin>>x1>>y1>>x2>>y2; d[x1] [y1] += 1; d[x2+1][y1] -= 1; d[x1] [y2+1] -= 1; d[x2+1][y2+1] += 1; // 右下角补回 } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ d[i][j] += d[i-1][j] + d[i][j-1] - d[i-1][j-1]; cout << d[i][j] << (j==n?'\n':' '); } } return 0; }
d[x2+1][y2+1] 是 +=1 不是 −=1,符号写反结果会整体错位;③ 输出行末不能有空格,用 (j==n?'\n':' ') 控制。激光炸弹:二维前缀和 + 枚举正方形
N 个带价值的点,一颗炸弹摧毁边长为 R 的正方形内所有目标,求最大总价值。
① 坐标平移
点的坐标从 0 开始,先 整体 +1 转成 1-based,避免下标 0 越界。
② 价值累加
同一坐标可能有多个点,用 s[x+1][y+1] += v 累加而不是赋值。
③ R 要截断
R 可能极大(甚至 10⁹)。地图最大边长为 5001,所以 R = min(R, 5001)。
s[i-R][j] 的 i-R 会变成很大的负数,直接数组越界——这是本题最隐蔽的 RE 点。0 ≤ x, y < 5001,即坐标最大 5000。转 1-based 后最大 5001。所以地图边长就是 5001,扫描上界取 5001、R 截断到 5001。别想当然写成 5000 或 n。激光炸弹 · 代码走读
const int M=5001; int s[M+2][M+2]; // 约 100MB,注意内存 int main(){ int N,R; cin>>N>>R; if(R>M) R=M; // ★ 截断,防越界 for(int i=0;i<N;i++){ int x,y,v; cin>>x>>y>>v; s[x+1][y+1] += v; // ★ +1 平移 + 累加 } for(int i=1;i<=M;i++) for(int j=1;j<=M;j++) s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1]; int ans=0; for(int i=R;i<=M;i++) for(int j=R;j<=M;j++) ans = max(ans, s[i][j]-s[i-R][j]-s[i][j-R]+s[i-R][j-R]); cout << ans << "\n"; }
ios::sync_with_stdio(false) 记得加,否则 25M 次循环 + 输入输出容易超时。二维专题高频易错辨析
① 二维前缀和的构建公式里,为什么要减 S[i-1][j-1]?
② 矩形 (x1,y1)~(x2,y2) 加 1,右下角 d[x2+1][y2+1] 应该?
③ 激光炸弹里 R 远大于地图边长时应当?
一道题 100 分是分档给的——不会正解也要先拿保底
复赛一道题的 100 分由十几到二十几个测试点组成, 前面几档专门为「没想出正解的人」准备。四道题各拿 30~40 分, 加起来往往就是一等奖和二等奖的分界。
① 特判档:先别想算法,看数据范围表
题目给的数据范围表就是出题人给你的送分清单。看到「n = 1」「只有一组数据」这类一行,
就先写个 if 直接输出答案。2 分钟换 5~10 分,全场最划算。
② 暴力档:按题意最直白地写一遍
多重循环、DFS 全枚举、朴素 O(n³)……不要优化,只要保证最小那档全对。 它既是保底分,又是后面对拍的标尺——没有它你无法证明正解是对的。
③ 特殊性质档:题目里那句「若……」
常见的送分性质:数据已经有序、所有值完全相同、规模小到可以 O(n²)、 只出现一种类型。题目写出来就是让你拿的,专门写一份即可。
④ 正解档:思路定了,就赢了八成
考场上的时间几乎都花在想思路上,不是写代码。写完先跑样例, 再用第 ② 档的暴力版造小数据对拍——这是唯一能证明你思路对的办法。
本讲 2 道题,各自的四档怎么走
地毯(二维差分) · CARPET
第1档 · 2 分钟n == 1 → 只有那一个矩形是 1
第2档 · 8 分钟
每张地毯逐格 +1,n ≤ 1000、区域 ≤ 1000² 稳过
第3档 · 10 分钟
所有地毯都是 1×1 → 就是二维计数,不用差分
第4档 · 15 分钟
四角 d[x1][y1]+=1, d[x1][y2+1]−=1, d[x2+1][y1]−=1, d[x2+1][y2+1]+=1 再二维前缀和
激光炸弹 · LASER
第1档 · 2 分钟R == 1 → 就是单格最大值;目标点全在 R 内 → 直接输出总和
第2档 · 8 分钟
枚举每个格子为左下角逐格求和,R ≤ 10、坐标 ≤ 5000 稳过
第3档 · 10 分钟
R 覆盖整个区域 → 直接输出总和
第4档 · 15 分钟
二维前缀和 + O(1) 查询每个 R×R 子矩阵,取 max
0–5 分钟 读题 + 圈出数据范围表里最小的那几档 → 5–15 分钟 写完第 1、2 档并先交一次(保住 20~30 分)→ 15–40 分钟 冲第 3、4 档 → 40–50 分钟 造小数据对拍、检查
freopen 与文件名。本讲知识树与自检清单
🌳 知识树
二维前缀和:S[i][j] = 上 + 左 − 左上 + 自己;子矩阵和 = 大 − 上 − 左 + 左上。
二维差分:矩形加只改 4 个角(右下角加回)。
流水线:差分改四角 → 二维前缀和还原 → 输出/取答案。
✅ 考场 N 步自检
- 下标从 1 开始了吗?坐标含 0 就整体 +1 了吗?
- 容斥符号对吗?记住「减两加一」。
- 数组开够 n+2 / M+2 了吗?
- 大数组是不是放在全局?(放 main 会爆栈)
- 范围参数(如 R)有没有截断到地图尺寸内?
- 值会不会超 int?叠加次数多时要考虑范围。