两个问题,看着相反,其实是同一件事。
A 的答案是前缀和:一次 O(n) 预处理,之后每次询问 O(1)。B 的答案是差分:每次修改 O(1),最后一次 O(n) 还原。两者互为逆运算——前缀和把“一段区间”换成“两个前缀相减”,差分把“一次区间修改”换成“两次单点修改”。
| 组成 | 含义 / 格式要求 |
|---|---|
pre[i] | 前 i 个数的和:pre[i] = a[1]+a[2]+…+a[i]。它存的是累计量,不是 a 本身。 |
pre[0] = 0 | 哨兵,它就是“前 0 个数的和”。有了它,l = 1 时 pre[l-1] 仍然是一个有定义的值,不用给 l = 1 写特判。 |
下标从 1 开始 | a[1..n] 放数据,pre[0..n] 放答案。这样 pre[r] - pre[l-1] 对 l = 1 也成立,不用特判。 |
sum(l, r) | pre[r] - pre[l-1]。减法把 a[1..l-1] 削掉,剩下的正好是 a[l..r]。 |
顺序 | 只能 i 从小到大递推,pre[i] 依赖 pre[i-1]。 |
量级 | n 个数的和最大 n × max|a|。n = 2×10⁵、a ≤ 10⁹ 时是 2×10¹⁴,必须 long long。 |
const int MAXN = 200005; // 按题目上限开
int n; long long a[MAXN], pre[MAXN];
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) pre[i] = pre[i - 1] + a[i]; // O(n) 建表
int q; cin >> q;
while (q--) {
int l, r; cin >> l >> r;
cout << pre[r] - pre[l - 1] << "\n"; // O(1) 查询
}pre 里任何一项算错,它后面全部错。调试时先打印整张 pre 和手算值比对。
pre 的元素类型要按“和”的量级定,不能照抄 a 的类型。
| 组成 | 含义 / 格式要求 |
|---|---|
d[i] | 相邻两项之差,记录 a 的变化量:d[i] = a[i] - a[i-1]。 |
区间加 z | d[l] += z; d[r+1] -= z; 两次单点修改,顶替一次 O(len) 的循环。 |
为什么 | d[l] 变大 → 前缀和还原时从 l 起每一项都多 z;d[r+1] 减回去 → 从 r+1 起恢复原状。 |
还原 | a[i] = a[i-1] + d[i],等价于对 d 做一遍前缀和。这一步必须做,不做就没有答案。 |
数组大小 | 要写 d[r+1],而 r 可以等于 n,所以 d 至少开 n+2 个。开 n 个一定越界。 |
复杂度 | 单次修改 O(1),最后还原 O(n)。q 次修改总代价 O(n+q),而不是 O(nq)。 |
// 目标:把 a[l..r] 每个数都加 z
d[l] += z;
d[r + 1] -= z; // r = n 时这句话写的是 d[n+1],所以要开 n+2
// 撤销差分,把 d 还原成 a
int cur = 0;
for (int i = 1; i <= n; ++i) {
cur += d[i]; // cur 就是还原后的 a[i]
}差分数组本身不是答案,它只是“改动记录”。输出前一定要前缀和还原。
d[r+1] 越界不会立刻报错,会改到隔壁变量上,症状是“某一小段数据莫名其妙不对”。
| 做法 | 预处理 | 一次区间和 / 一次区间加 | n = q = 10⁵ 时 |
|---|---|---|---|
| 朴素循环 | 无 | O(n) | 10¹⁰ 次,TLE |
| 前缀和 | O(n) 建表 | O(1) 查询 | 10⁵ 次,AC |
| 差分 | O(n) 还原 | O(1) 修改 | 10⁵ 次,AC |
有 n 个学生,给出初始成绩 a₁…aₙ。接下来 p 次操作,每次把第 x 个到第 y 个学生的成绩每人加 z 分。所有操作结束后,输出全班的最低分。
输入:第一行 n、p(n ≤ 5×10⁶,p ≤ n);第二行 n 个整数表示初始成绩(≤ 100);接下来 p 行,每行 x、y、z(1 ≤ x ≤ y ≤ n,z ≤ 100),表示给第 x 到第 y 个学生每人加 z 分。
输出:一行一个整数,即最终的最低分。
朴素做法:每次操作 for 一遍 [x, y]。p 和 n 都能到 5×10⁶,最坏 2.5×10¹³ 次加法,必然 TLE。
换成差分。整个程序只有三段:建差分 → 2p 次单点修改 → 一遍前缀和还原并取最小。中间那 p 次操作每次只碰两个格子,与区间长度无关,这是它能从 O(np) 降到 O(n+p) 的全部原因。
两个细节。① z 的符号不用管:z 为负时 d[x] += z 就是在做减法,算法一字不改。② 没必要把还原后的成绩再存一个数组:边还原边取 min,空间从 3 个数组降到 2 个。
内存算一下:n = 5×10⁶,两个 int 数组各 5×10⁶ × 4B = 20MB,合计 40MB,全局数组放静态区,不会爆栈。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5000005; // 题目上限 5×10⁶,再多留几个位给 d[n+1]
int a[MAXN], d[MAXN]; // a 存初始成绩,d 存差分
int main() {
ios::sync_with_stdio(false); // 5×10⁶ 个整数,必须提速
cin.tie(nullptr);
int n, p;
cin >> n >> p;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i)
d[i] = a[i] - a[i - 1]; // a[0] 是全局数组,默认 0,所以 d[1] = a[1] 自动成立
while (p--) {
int x, y, z;
cin >> x >> y >> z;
d[x] += z; // 从 x 开始,每个还原值都比原来多 z
d[y + 1] -= z; // 从 y+1 开始把这层 z 扣掉,恢复原样
}
int cur = 0, mn = INT_MAX;
for (int i = 1; i <= n; ++i) {
cur += d[i]; // 前缀和还原:cur 就是最终的 a[i]
if (cur < mn) mn = cur; // 边还原边统计最低分,省下第三个数组
}
cout << mn << "\n";
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
const int MAXN = 5000005; | 比 5×10⁶ 多留 5 个位置。y = n 时那句 d[y+1] 会写到下标 n+1,数组必须容得下。 |
int a[MAXN], d[MAXN]; | 全局数组默认全 0:a[0] = 0 让 d[1] 的公式不用特判,超出 n 的 d 值也是 0。另外 40MB 放静态区,放栈上会直接栈溢出。 |
ios::sync_with_stdio(false); | 5×10⁶ 次 cin 不关同步可能吃掉接近 1 秒,很可能 TLE。 |
d[i] = a[i] - a[i - 1]; | 差分定义式。i = 1 时 d[1] = a[1] - a[0] = a[1],靠 a[0] = 0 自动正确。 |
d[x] += z; | 让第 x 个位置开始“多出一层 z”。 |
d[y + 1] -= z; | 在 y 之后立刻削掉这层 z。两句合起来,只影响 [x, y]。 |
cur += d[i]; | 前缀和还原。d 里只有 O(p) 个非零值,cur 把它们的贡献一路累加下去。 |
if (cur < mn) mn = cur; | 最低分就在还原过程中统计,不再多开数组存最终成绩。 |
cout << mn << "\n"; | 题目只要一个整数加换行。 |
把数轴换成 n 行 m 列的网格,两类问题照抄:多次询问子矩阵和用二维前缀和;多次矩形加、最后输出整张表用二维差分。区别只有一个:一维里是“两次端点”,二维里是“四个角”。
| 组成 | 含义 / 格式要求 |
|---|---|
pre[i][j] | 左上角 (1,1)、右下角 (i,j) 这个矩形里所有数之和。 |
+ pre[i-1][j] | 上面那一大块:前 i-1 行、前 j 列。 |
+ pre[i][j-1] | 左边那一大块:前 i 行、前 j-1 列。 |
- pre[i-1][j-1] | 左上角那块被加了两次,减回一次——这就是容斥。 |
+ a[i][j] | 最后加上自己这一格。建表的四个符号是 + + − +。 |
查询 | 左上 (x1,y1) 到右下 (x2,y2):pre[x2][y2] − pre[x1-1][y2] − pre[x2][y1-1] + pre[x1-1][y1-1],符号是 + − − +。 |
数组大小 | 开 (n+1) × (m+1),下标从 1 开始,第 0 行、第 0 列全 0,边界不用特判。 |
// 建表:i、j 都必须从小到大(依赖上方和左方)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + a[i][j];
// 查询子矩阵和
long long s = pre[x2][y2] - pre[x1-1][y2] - pre[x2][y1-1] + pre[x1-1][y1-1];数一下就知道对不对:把 a = [[1,2],[3,4]] 代进去,pre[2][2] = 3 + 4 - 1 + 4 = 10 = 1+2+3+4。
n、m 都是 10³、每格 10⁹ 时,子矩阵和能到 10¹⁵,pre 必须是 long long。
| 组成 | 含义 / 格式要求 |
|---|---|
四个角 | 左上、左下、右上、右下各改一次,一次“矩形加 z”就是 O(1)。 |
符号规律 | 对角同号:左上、右下 +z;右上、左下 −z。 |
为什么 | 还原后,一个角上的 +z 会向右下扩散成整块 +z。矩形内的格子只吃到左上角那一份 z;下边界外的被 (x2+1,y1) 的 −z 抵掉,右边界外的被 (x1,y2+1) 的 −z 抵掉,两者都超出的再由右下角的 +z 补平。 |
还原 | 对 d 做一次二维前缀和:d[i][j] += d[i-1][j] + d[i][j-1] - d[i-1][j-1]。还原后 d[i][j] 才是真值。 |
数组大小 | 要写 x2+1、y2+1,它们可以等于 n+1、m+1,所以开 (n+2) × (m+2)。 |
注意 | 还原是“就地覆盖”:还原过的 d 不再是差分,别拿它再做修改。 |
// 矩形 (x1,y1)-(x2,y2) 每个格子加 z
d[x1][y1] += z;
d[x2 + 1][y1] -= z;
d[x1][y2 + 1] -= z;
d[x2 + 1][y2 + 1] += z;
// 还原:做完这一步 d[i][j] 才是格子 (i,j) 的真实值
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
d[i][j] += d[i-1][j] + d[i][j-1] - d[i-1][j-1];四个角写成“上面两个减、下面两个加”也能算,但符号必须与位置对应;对角的两个永远同号。
m 个矩形是 4m 次修改,加上 O(nm) 还原,总复杂度 O(nm + m),可以从 10³×10³ 过。
| 场景 | 公式 | 符号 |
|---|---|---|
| 建二维前缀和 | pre[i][j] = pre[i-1][j] + pre[i][j-1] − pre[i-1][j-1] + a[i][j] | + + − + |
| 查询子矩阵和 | pre[x2][y2] − pre[x1-1][y2] − pre[x2][y1-1] + pre[x1-1][y1-1] | + − − + |
| 二维差分修改 | 左上、右下 +z;右上、左下 −z | + − − + |
| 二维差分还原 | d[i][j] += d[i-1][j] + d[i][j-1] − d[i-1][j-1] | + + − |
RE / 随机 WA:d 只开了 n 个,而 d[r+1] 在 r = n 时写到下标 n+1。开 n+2。
WA 一整片:忘了还原,直接把 d 输出了。d 里大多是 0 和很小的数,和答案差得离谱。
WA 一半:d[r+1] -= z 写成 d[r] -= z。症状是每个区间的右端点那一格没被加上,短区间自查不出来,长区间一错错一整段。
WA 全负:建差分时写成 d[i] = a[i] - a[i+1],符号反了,还原后整条数组变号。
溢出:和用 int 存。本题最坏 100 + 5×10⁶ × 100 = 5×10⁸,int 刚好够;把 z 上限看成 10⁹ 就必然爆。
TLE:n = 5×10⁶ 却没写 ios::sync_with_stdio(false)。
符号记反:子矩阵查询最后一项写成减号。症状:答案偏小,且小的正好是 (x1-1)×(y1-1) 那块的和。
只开 n × m:二维差分要写 d[x2+1][...]、d[...][y2+1],x2 = n 时下标是 n+1。开 (n+2) × (m+2)。
还原顺序错:i 或 j 从大到小、或者行列写反(把 x 当列用)。前者用到的是旧值,后者输出整个转置,样例第一行就对不上。
关掉页面,从空文件把 P2367 敲一遍并提交。先把 ios::sync_with_stdio(false) 注释掉测一次,感受 5×10⁶ 个整数的读入差距。
改一改 ①:删掉数组 a,读入时直接写进 d,然后 for (int i = n; i >= 1; --i) d[i] -= d[i-1]; 原地差分。内存从 40MB 降到 20MB,答案必须一模一样。
改一改 ②:把 d 改成 int d[n+1](正好 n+1 个),输入一组 y = n 的数据,例如 3 1 / 1 1 1 / 1 3 5,看它是否给出错误结果——亲手见一次越界。
改一改 ③:题目改成“输出操作后的总分”,只需把取 min 改成累加 cur,其余不动。
记 S 为前缀和算子:(Sa)[i] = a[1] + a[2] + … + a[i]。记 D 为差分算子:(Da)[i] = a[i] − a[i−1],约定 a[0] = 0。
那么 D(Sa) = a,S(Da) = a。二者互为逆运算。写成矩阵(n = 3)看得最清楚:
S = [1 0 0] D = [ 1 0 0] D * S = I
[1 1 0] [-1 1 0]
[1 1 1] [ 0 -1 1]两个技巧就是同一句话的两种读法:
数字验一遍。a = [1, 2, 3],pre = [0, 1, 3, 6],区间 [2, 3] 的和 = pre[3] − pre[1] = 6 − 1 = 5 = 2 + 3。✓
d = [1, 1, 1]。给 [2, 3] 加 4:d[2] += 4 → d = [1, 5, 1];d[4] −= 4 → d 的第 4 位变成 −4。还原 cur 依次为 1、6、7(再往下累加会得到 3,那是数组外的位置,不用管)→ a = [1, 6, 7] = [1, 2, 3] + [0, 4, 4]。✓
二维的容斥来自 |A ∪ B| = |A| + |B| − |A ∩ B|。建表时“上”和“左”两块都把左上角算了一遍,所以要减一次;查询时要去掉上方、左方两块,而它们的交集(左上角)被减了两次,所以要加回一次。二维差分的四个角是同一件事的另一种用法:一个角的 +z 还原后扩散成右下角整块 +z,四个角叠加就把“矩形”圈了出来。
最后一条推论:凡是“可减”的运算,前缀和都能用。加法可以(减法撤销),异或可以(自己撤销自己);max、min 没有逆运算,所以区间最大值不能用前缀和,要做 ST 表。
在 n×n 的格子上铺了 m 块矩形地毯,给出每块地毯的左上角 (x₁, y₁) 和右下角 (x₂, y₂)。求每个格子被多少块地毯覆盖。
输入:第一行两个正整数 n、m(n, m ≤ 1000);接下来 m 行,每行四个整数 x₁、y₁、x₂、y₂(1 ≤ x₁ ≤ x₂ ≤ n,1 ≤ y₁ ≤ y₂ ≤ n)。
输出:n 行,每行 n 个整数,第 i 行第 j 个数表示格子 (i, j) 被覆盖的次数,用空格分隔。
一块地毯的效果是“矩形内每格 +1”,先想清楚这等价于在四个位置上各做一次单点修改。
所有修改都记下来之后,一次二维前缀和就能把它们“推”到每个格子上;顺序必须从左上到右下。
矩形的右下角是 (x₂, y₂),所以会用到 x₂+1 和 y₂+1——数组要留出这一行一列。
把每块地毯看成一次“矩形 +1”。用二维差分 d:对矩形 (x1,y1)-(x2,y2) 执行 d[x1][y1]++、d[x2+1][y1]--、d[x1][y2+1]--、d[x2+1][y2+1]++。
m 块地毯就是 4m 次 O(1) 修改,总代价 O(m)。
最后对 d 做一次二维前缀和(就地覆盖),d[i][j] 直接变成格子 (i,j) 的覆盖数。
为什么四个角能圈出矩形:还原时一个角上的 +1 会向右下扩散成“从这个角到右下角”的整块 +1。矩形内的格子只吃到左上角那一份 +1;跑到下边界之外的格子被 (x2+1,y1) 的 −1 抵掉,跑到右边界之外的被 (x1,y2+1) 的 −1 抵掉,两者都超出的再被右下角的 +1 补平,全是 0。
复杂度 O(n² + m),n = m = 1000 时约 10⁶ 次操作,远小于 10⁸。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005; // n ≤ 1000,还要用下标 n+1,所以多开几个
int d[MAXN][MAXN];
int main() {
ios::sync_with_stdio(false); // n² = 10⁶ 次输出,必须提速
cin.tie(nullptr);
int n, m;
cin >> n >> m;
while (m--) {
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[x1][y1] += 1; | 左上角 +1:还原后,这个 +1 会向整块右下区域扩散。 |
d[x2 + 1][y1] -= 1; | 矩形下边界外面第一行开始,把扩散出来的覆盖撤掉。 |
d[x1][y2 + 1] -= 1; | 右边界外面第一列开始,同样撤掉。 |
d[x2 + 1][y2 + 1] += 1; | 右下角外侧被上面两句各撤了一次,共撤两次,所以要补回一次。 |
d[i][j] += d[i-1][j] + d[i][j-1] - d[i-1][j-1]; | 二维前缀和还原,就地覆盖;此时 d[i][j] 就是答案,不用另开数组。 |
(j == n ? '\n' : ' ') | 行末只换行不加空格,避免被严格评测判 WA。 |
MAXN = 1005 | x2 = n 时会写 d[n+1][...],下标要能到 1001,开 1005 稳妥。 |