← 目录 第 18 / 60 节 全部 60 节

18 · 前缀和与差分

两个互逆的算子:前缀和把区间查询降到 O(1),差分把区间修改降到 O(1)。一维练熟以后平移到二维,掌握容斥的四个符号和差分的四个角。
⭐ 已学范围普及-预计 60 分钟前缀和差分二维容斥
本节出现的新函数 / 新语法(先看懂再用)

两个问题,看着相反,其实是同一件事。

A 的答案是前缀和:一次 O(n) 预处理,之后每次询问 O(1)。B 的答案是差分:每次修改 O(1),最后一次 O(n) 还原。两者互为逆运算——前缀和把“一段区间”换成“两个前缀相减”,差分把“一次区间修改”换成“两次单点修改”。

“多次询问区间”往前缀和想,“多次修改区间、最后统一输出”往差分想。别混:前缀和单次修改是 O(n),差分单次询问是 O(n)。
新函数拆解 · pre 数组(一维前缀和)
pre[0] = 0; pre[i] = pre[i-1] + a[i]; // i = 1..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 数组(一维差分)
d[1] = a[1]; d[i] = a[i] - a[i-1]; // i = 2..n
组成含义 / 格式要求
d[i]相邻两项之差,记录 a 的变化量:d[i] = a[i] - a[i-1]。
区间加 zd[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
例题 · 语文成绩洛谷 P2367普及-去洛谷做这题 ↗

有 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 分。

输出:一行一个整数,即最终的最低分。

输入: 3 2 1 1 1 1 2 1 2 3 1 输出: 2
例题怎么想:把 p 次区间加法换成 2p 次单点修改

朴素做法:每次操作 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 列的网格,两类问题照抄:多次询问子矩阵和用二维前缀和;多次矩形加、最后输出整张表用二维差分。区别只有一个:一维里是“两次端点”,二维里是“四个角”。

新函数拆解 · pre2 数组(二维前缀和)
pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + a[i][j];
组成含义 / 格式要求
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。

新函数拆解 · d2 数组(二维差分)
d[x1][y1] += z; d[x2+1][y1] -= z; d[x1][y2+1] -= z; d[x2+1][y2+1] += z;
组成含义 / 格式要求
四个角左上、左下、右上、右下各改一次,一次“矩形加 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]+ + −
差分是“记账”,不是“结账”。账记完(改完 d)以后,必须做一次前缀和把它翻译回真实值,这一步在二维里尤其容易忘。
差分的 6 种典型错法(含症状)

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)。

二维专属的 3 个坑

符号记反:子矩阵查询最后一项写成减号。症状:答案偏小,且小的正好是 (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,D 是对角 1、下对角 −1
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 表。

举一反三 · 想清楚这些再往下走
  1. 区间查询换成“区间最大值”,前缀和还能用吗?从“有没有逆运算”这个角度回答,并说出正确做法需要预处理什么。
  2. 给 n 个数,问有多少个子区间的和能被 7 整除。把前缀和对 7 取模后,答案为什么等于“相同余数的前缀两两配对”?写出计数式子。
  3. 差分再差分一次得到二阶差分。如果要“把 [l, r] 加上首项 p、公差 q 的等差数列”,二阶差分要改几个位置?分别是哪几个?
  4. 给一个 n×m 矩阵,求元素和最大的 h×w 子矩形。二维前缀和能把每块的和算成 O(1),那总复杂度是多少?还有更快的吗?
  5. 如果操作是“把 [l, r] 全部赋值为 z”(不是加 z),差分还够用吗?需要额外记录什么信息?
自测:能全部回答再进下一节
挑战题 · 地毯
洛谷 P3397普及-去洛谷做这题 ↗

在 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 = 1005x2 = n 时会写 d[n+1][...],下标要能到 1001,开 1005 稳妥。
这题最容易错的地方
  • 数组只开到 1000 或 1001:x2 = n 时 d[n+1][y1] 越界,表现是随机 WA 或 RE。开 1005。
  • 四个角位置写错,例如把 d[x2+1][y1] 写成 d[x1+1][y1],结果是每块矩形都少算一条边。
  • 忘记还原直接输出 d:只有四个角非零,样例第一行就对不上。
  • 还原时内层循环写成 m 而不是 n,或者把 i、j 当成 (y, x) 用:整张表转置,全错。
延伸阅读 / 参考