📖 第 3.2 章 ⏱️ 约 70 分钟 🎯 中级

第 3.2 章:数组与前缀和

📝 前置条件: 确保你熟悉数组、向量和基本循环(第 2.2–2.3 章)。还需要理解 long long 溢出(第 2.1 章)。

设想你有一个 N 个数字的数组,有人问你 100,000 次:「从下标 L 到 R 的元素之和是多少?」朴素做法每次重新计算——每次查询 O(N),总计 O(N × Q)。当 N = Q = 10^5 时,是 10^10 次操作,远远太慢。

前缀和O(N) 预处理、每次查询 O(1) 解决这个问题。这是竞赛编程中最优雅、最实用的技术之一。

💡 核心思路: 前缀和将「区间查询」问题转化为一次减法。不用每次都从 L 到 R 求和,而是预计算累积和后做两次相减。这把 O(Q) 的重复计算换成了一次性的 O(N) 预处理。


3.2.1 前缀和的思想

数组的前缀和是一个新数组,其中每个元素存储到当前下标为止的累积和。

图示:前缀和数组

Prefix Sum Visualization

上图展示了如何从原始数组构建前缀和数组,以及如何用 sum(L, R) = P[R] - P[L-1]O(1) 时间内计算区间和。蓝色单元格标示查询范围,红绿单元格展示被相减的两个前缀值。

给定数组:A = [3, 1, 4, 1, 5, 9, 2, 6](使用 1-indexed 以便说明)

下标:  1  2  3  4  5  6  7  8
A:      3  1  4  1  5  9  2  6
P:      3  4  8  9  14 23 25 31

其中 P[i] = A[1] + A[2] + ... + A[i]

为什么用 1-indexed?

使用 1-indexed 数组让我们可以定义 P[0] = 0(「空前缀」和为零)。这使得查询公式 P[R] - P[L-1]L = 1 时也能正常工作——计算 P[R] - P[0] = P[R],这是正确的。

构建前缀和数组

📄 查看代码:构建前缀和数组
// 构建前缀和数组 — O(N)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    // 第一步:读取输入(1-indexed)
    vector<int> A(n + 1);
    for (int i = 1; i <= n; i++) cin >> A[i];

    // 第二步:构建前缀和
    vector<long long> P(n + 1, 0);  // P[0] = 0(基础情况)
    for (int i = 1; i <= n; i++) {
        P[i] = P[i - 1] + A[i];   // ← 关键行:每个 P[i] = 到 i 为止所有元素之和
    }

    return 0;
}

复杂度分析:

  • 时间: O(N) —— 遍历数组一次
  • 空间: O(N) —— 存储前缀数组

A = [3, 1, 4, 1, 5] 的逐步追踪:

i=1: P[1] = P[0] + A[1] = 0 + 3 = 3
i=2: P[2] = P[1] + A[2] = 3 + 1 = 4
i=3: P[3] = P[2] + A[3] = 4 + 4 = 8
i=4: P[4] = P[3] + A[4] = 8 + 1 = 9
i=5: P[5] = P[4] + A[5] = 9 + 5 = 14

3.2.2 O(1) 区间求和查询

有了前缀和数组,下标 L 到 R 的和就是:

sum(L, R) = P[R] - P[L-1]

为什么? P[R] = 1..R 的元素之和。P[L-1] = 1..(L-1) 的元素之和。两者之差 = L..R 的元素之和。

💡 核心思路: 把 P[i] 理解为「前 i 个元素的总和」。要得到窗口 [L, R] 的和,就从「到 R 的前缀」中减去「L 之前的前缀」。就像:大三角形减去小三角形 = 梯形。

📄 C++ 完整代码
// 区间求和查询 — 预处理 O(N),每次查询 O(1)
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100001;
long long A[MAXN];
long long P[MAXN];

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, q;
    cin >> n >> q;

    // 第一步:读取数组
    for (int i = 1; i <= n; i++) cin >> A[i];

    // 第二步:构建前缀和 — O(n)
    P[0] = 0;
    for (int i = 1; i <= n; i++) {
        P[i] = P[i - 1] + A[i];
    }

    // 第三步:回答 q 个区间求和查询 — 每次 O(1)
    for (int i = 0; i < q; i++) {
        int l, r;
        cin >> l >> r;
        cout << P[r] - P[l - 1] << "\n";  // ← 关键行:区间和公式
    }

    return 0;
}

样例输入:

8 3
3 1 4 1 5 9 2 6
1 4
3 7
2 6

样例输出:

9
21
20

验证:

  • sum(1,4) = P[4] - P[0] = 9 - 0 = 9 → A[1]+A[2]+A[3]+A[4] = 3+1+4+1 = 9 ✓
  • sum(3,7) = P[7] - P[2] = 25 - 4 = 21 → A[3]+...+A[7] = 4+1+5+9+2 = 21 ✓
  • sum(2,6) = P[6] - P[1] = 23 - 3 = 20 → A[2]+...+A[6] = 1+4+1+5+9 = 20 ✓

⚠️ 常见错误: 写成 P[R] - P[L] 而不是 P[R] - P[L-1]。公式包含 L 和 R 两个端点——你要减去 L 之前的和,不是 L 的和。

总复杂度: O(N + Q) —— 对 N、Q 最大 10^5 完全没问题。


3.2.3 USACO 示例:品种统计

这是一道经典的 USACO Bronze 题(2015 年 12 月)。

题目: N 头奶牛排成一列,每头的品种是 1、2 或 3。回答 Q 个查询:位置 L 到 R 中有多少头品种为 B 的奶牛?

解法: 每种品种维护一个前缀和数组。

📄 C++ 完整代码
// 多品种前缀和 — O(N + Q)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, q;
    cin >> n >> q;

    vector<int> breed(n + 1);
    vector<vector<long long>> P(4, vector<long long>(n + 1, 0));
    // P[b][i] = 位置 1..i 中品种 b 的奶牛数量

    // 第一步:为每种品种构建前缀和
    for (int i = 1; i <= n; i++) {
        cin >> breed[i];
        for (int b = 1; b <= 3; b++) {
            P[b][i] = P[b][i - 1] + (breed[i] == b ? 1 : 0);  // ← 关键行
        }
    }

    // 第二步:每次查询 O(1) 回答
    for (int i = 0; i < q; i++) {
        int l, r, b;
        cin >> l >> r >> b;
        cout << P[b][r] - P[b][l - 1] << "\n";
    }

    return 0;
}

🏆 USACO 技巧: 很多 USACO Bronze 题涉及「统计范围内满足属性 X 的元素个数」。如果 Q 较大,始终考虑前缀和。


3.2.4 USACO 风格题目详解:FJ 的草地

🔗 相关题目: 这是一道受「品种统计」和「最高奶牛」启发的虚构 USACO 风格题目——两者都是经典 Bronze 题。

题目: FJ 有 N 块连续的田地,第 i 块有 grass[i] 单位的草。他需要回答 Q 个查询:「第 L 块到第 R 块(含)的草总量是多少?」N、Q 最大 10^5,每次查询需要 O(1) 回答。

样例输入:

6 4
4 2 7 1 8 3
1 3
2 5
4 6
1 6

样例输出:

13
18
12
25

逐步解法:

第一步: 理解题目。我们有数组 [4, 2, 7, 1, 8, 3],需要区间求和。

第二步: 构建前缀和数组。

下标:  0  1  2  3  4  5  6
草量:  -  4  2  7  1  8  3
P:      0  4  6  13 14 22 25

第三步:P[R] - P[L-1] 回答查询:

  • 查询 (1,3):P[3] - P[0] = 13 - 0 = 13
  • 查询 (2,5):P[5] - P[1] = 22 - 4 = 18
  • 查询 (4,6):P[6] - P[3] = 25 - 13 = 12
  • 查询 (1,6):P[6] - P[0] = 25 - 0 = 25

完整 C++ 解法:

📄 C++ 完整代码
// FJ 的草地 — 前缀和解法 O(N + Q)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, q;
    cin >> n >> q;

    // 第一步:读取草量并同步构建前缀和
    vector<long long> P(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        long long g;
        cin >> g;
        P[i] = P[i - 1] + g;   // ← 关键行:增量前缀和
    }

    // 第二步:每次查询 O(1) 回答
    while (q--) {
        int l, r;
        cin >> l >> r;
        cout << P[r] - P[l - 1] << "\n";
    }

    return 0;
}

为什么是 O(N + Q)

  • 构建前缀和:一个循环,N 次迭代 → O(N)
  • 每次查询:一次减法 → 每次 O(1),共 O(Q)
  • 总计:O(N + Q) —— 远好于暴力的 O(NQ)

⚠️ 常见错误: 前缀和用 int 而不是 long long。如果草量最大 10^9 且 N = 10^5,总和可达 10^14——远超 int 约 2×10^9 的范围。


3.2.5 二维前缀和

对于二维网格,可以扩展前缀和在 O(1) 时间内回答矩形区间查询。

给定 R×C 的网格,定义 P[r][c] = 从 (1,1) 到 (r,c) 矩形内所有元素之和。

构建二维前缀和

P[r][c] = A[r][c] + P[r-1][c] + P[r][c-1] - P[r-1][c-1]

减法是为了消除重叠(否则左上角矩形会被计算两次)。

💡 核心思路(容斥原理): 想象四个矩形:

  • P[r-1][c] = 「上方」矩形
  • P[r][c-1] = 「左方」矩形
  • P[r-1][c-1] = 「左上角」(在上面两个中都计算了——所以减去一次)
  • A[r][c] = 单个新单元格

二维前缀和逐步工作示例

追踪一个 4×4 网格:

原始网格 A:

     c=1  c=2  c=3  c=4
r=1:  1    2    3    4
r=2:  5    6    7    8
r=3:  9   10   11   12
r=4: 13   14   15   16

逐步构建 P(从左到右、从上到下):

📄 Code 完整代码
P[1][1] = A[1][1] = 1
P[1][2] = 2 + 0 + 1 - 0 = 3
P[1][3] = 3 + 0 + 3 - 0 = 6
P[1][4] = 4 + 0 + 6 - 0 = 10
P[2][1] = 5 + 1 + 0 - 0 = 6
P[2][2] = 6 + 3 + 6 - 1 = 14
P[2][3] = 7 + 6 + 14 - 3 = 24
P[2][4] = 8 + 10 + 24 - 6 = 36
P[3][1] = 9 + 6 + 0 - 0 = 15
P[3][2] = 10 + 14 + 15 - 6 = 33
P[3][3] = 11 + 24 + 33 - 14 = 54
P[3][4] = 12 + 36 + 54 - 24 = 78
P[4][1] = 13 + 15 + 0 - 0 = 28
P[4][2] = 14 + 33 + 28 - 15 = 60
P[4][3] = 15 + 54 + 60 - 33 = 96
P[4][4] = 16 + 78 + 96 - 54 = 136

前缀和网格 P:

     c=1  c=2  c=3  c=4
r=1:  1    3    6   10
r=2:  6   14   24   36
r=3: 15   33   54   78
r=4: 28   60   96  136

查询:子网格 (r1=2, c1=2) 到 (r2=3, c2=3) 的和:

ans = P[3][3] - P[1][3] - P[3][1] + P[1][1]
    = 54     -  6     -  15     +  1
    = 34

验证:A[2][2]+A[2][3]+A[3][2]+A[3][3] = 6+7+10+11 = 34 ✓

容斥原理图示:

2D Prefix Sum Inclusion-Exclusion

📄 C++ 完整代码
// 二维前缀和 — 构建 O(R×C),查询 O(1)
#include <bits/stdc++.h>
using namespace std;

const int MAXR = 1001, MAXC = 1001;
int A[MAXR][MAXC];
long long P[MAXR][MAXC];

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int R, C;
    cin >> R >> C;

    for (int r = 1; r <= R; r++)
        for (int c = 1; c <= C; c++)
            cin >> A[r][c];

    // 第一步:构建二维前缀和 — O(R × C)
    for (int r = 1; r <= R; r++) {
        for (int c = 1; c <= C; c++) {
            P[r][c] = A[r][c]
                    + P[r-1][c]    // 上方矩形
                    + P[r][c-1]    // 左方矩形
                    - P[r-1][c-1]; // ← 关键行:消除重叠(被计算了两次)
        }
    }

    // 第二步:每次查询 O(1) 回答
    int q;
    cin >> q;
    while (q--) {
        int r1, c1, r2, c2;
        cin >> r1 >> c1 >> r2 >> c2;
        long long ans = P[r2][c2]
                      - P[r1-1][c2]    // 减去上方条带
                      - P[r2][c1-1]    // 减去左方条带
                      + P[r1-1][c1-1]; // 加回左上角
        cout << ans << "\n";
    }

    return 0;
}

复杂度分析:

  • 构建时间: O(R × C)
  • 查询时间: 每次 O(1)
  • 空间: O(R × C)

⚠️ 常见错误: 查询公式中忘记加回 P[r1-1][c1-1]。上方条带和左方条带都包含左上角,所以被减了两次——需要加回一次!


3.2.6 差分数组

前面我们学习了前缀和:它擅长解决「很多次区间求和」问题。

现在来看一个看起来相反的问题:

有一个长度为 N 的数组,一开始全是 0。接下来有很多次操作,每次都要把一整段 [L, R] 全部加上一个值 V。最后输出整个数组。

如果直接模拟,每次操作都从 L 循环到 R

for (int i = L; i <= R; i++) {
    A[i] += V;
}

这当然能做对,但如果 N 和操作次数 M 都是 100000,最坏情况下就是 10^10 次加法,太慢。

差分数组解决的核心问题就是:

如何不用真的修改区间里的每一个元素,也能表达「整个区间都加了 V」?


先从一个生活例子理解

想象一条直线跑道,有 8 个位置:

位置: 1  2  3  4  5  6  7  8
数值: 0  0  0  0  0  0  0  0

现在要把 [3, 6] 全部加 5。

朴素做法是给 3、4、5、6 每个位置都写上 +5

但差分数组不这样做。它只记录两件事:

从位置 3 开始,加 5 生效
从位置 7 开始,加 5 失效

也就是:

diff[3] += 5;
diff[7] -= 5;

为什么这样就够了?因为最后我们会从左到右累加这些「变化标记」。

diff: 0  0 +5  0  0  0 -5  0
累加: 0  0  5  5  5  5  0  0
位置: 1  2  3  4  5  6  7  8

你会发现,位置 3 到 6 正好都变成了 5,位置 7 开始又恢复为 0。

这就是差分数组的全部直觉:

区间更新不直接改区间,而是在区间起点放一个「开始变化」标记,在区间终点后一个位置放一个「停止变化」标记。

Difference Array Range Update


差分数组到底存的是什么?

如果原数组是:

A:     2   5   5   9   7

那么差分数组 diff 存的是「当前位置相对于前一个位置变了多少」:

diff[1] = A[1] - 0    = 2
diff[2] = A[2] - A[1] = 3
diff[3] = A[3] - A[2] = 0
diff[4] = A[4] - A[3] = 4
diff[5] = A[5] - A[4] = -2

所以:

A:     2   5   5   9   7
diff:  2   3   0   4  -2

如果对 diff 做前缀和,就能还原 A

2
2 + 3 = 5
2 + 3 + 0 = 5
2 + 3 + 0 + 4 = 9
2 + 3 + 0 + 4 - 2 = 7

因此可以把差分数组理解成:

diff[i] 记录的是「从第 i 个位置开始,数值发生了多少变化」。


为什么区间加法只需要改两个位置?

假设要给 [L, R] 都加上 V

从位置 L 开始,后面的值都应该多 V,所以:

diff[L] += V;

但是这个影响只能持续到 R,从 R+1 开始就不应该再多 V,所以要抵消掉:

diff[R + 1] -= V;

合起来就是差分数组最重要的公式:

diff[L] += V;
diff[R + 1] -= V;

注意:这就是为什么 diff 通常要开到 n + 2。当 R = n 时,我们仍然会访问 diff[n + 1]


完整例子:三次区间更新

从长度为 5 的全零数组开始:

A:     0  0  0  0  0

执行三次操作:

1) [1, 3] 加 2
2) [2, 5] 加 3
3) [3, 4] 加 -1

我们不直接修改 A,只修改 diff

第 1 次操作:[1, 3] +2

diff[1] += 2;
diff[4] -= 2;
diff[1..6] = [2, 0, 0, -2, 0, 0]

含义是:从 1 开始多 2,从 4 开始取消这次 +2。

第 2 次操作:[2, 5] +3

diff[2] += 3;
diff[6] -= 3;
diff[1..6] = [2, 3, 0, -2, 0, -3]

第 3 次操作:[3, 4] -1

diff[3] += -1;
diff[5] -= -1;   // 等价于 diff[5] += 1
diff[1..6] = [2, 3, -1, -2, 1, -3]

现在对 diff 做前缀和,还原最终数组:

i=1: 2            -> A[1] = 2
i=2: 2 + 3 = 5    -> A[2] = 5
i=3: 5 - 1 = 4    -> A[3] = 4
i=4: 4 - 2 = 2    -> A[4] = 2
i=5: 2 + 1 = 3    -> A[5] = 3

最终结果:

A:2 5 4 2 3

Difference Array Reconstruction


完整 C++ 实现

📄 C++ 完整代码
// 差分数组实现区间更新 — O(N + M)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, m;
    cin >> n >> m;

    // diff[i] 表示从位置 i 开始,当前值发生了多少变化
    // 多开 1 位:当更新区间右端点 r = n 时,需要访问 diff[n + 1]
    vector<long long> diff(n + 2, 0);

    // 第一步:处理所有区间更新
    for (int i = 0; i < m; i++) {
        int l, r;
        long long v;
        cin >> l >> r >> v;

        diff[l] += v;      // 从 l 开始,增加 v 生效
        diff[r + 1] -= v;  // 从 r+1 开始,取消这次增加
    }

    // 第二步:对 diff 做前缀和,得到最终数组
    long long cur = 0;
    for (int i = 1; i <= n; i++) {
        cur += diff[i];
        cout << cur;
        if (i < n) cout << " ";
    }
    cout << "\n";

    return 0;
}

样例输入:

5 3
1 3 2
2 5 3
3 4 -1

样例输出:

2 5 4 2 3

什么时候用差分数组?

差分数组适合这种场景:

很多次区间修改,最后统一查询结果

例如:

  • [L, R] 所有元素加上 V
  • 多次刷墙,每次刷一段区间
  • 多次给道路区间增加交通量
  • 多次给一段时间内的在线人数加 1

但要注意:

普通差分数组不适合「一边修改、一边查询区间和」的动态问题。

如果题目要求更新和查询交替出现,通常需要树状数组或线段树。


差分数组常见错误

  1. 数组开小了: diff 应该开 n + 2,因为会访问 diff[r + 1]
  2. 忘记最后做前缀和: diff 本身不是答案,前缀和之后才是最终数组。
  3. diff[r + 1] -= v 写成 diff[r] -= v 这样会让位置 r 没有被加到。
  4. 数值可能溢出: 多次区间加法后数值可能很大,建议用 long long

一句话记忆:

左端点开始加,右端点后停止加;最后扫一遍,变化变成答案。


3.2.7 二维差分数组

一维差分数组解决的是「给一段连续区间加值」。

二维差分数组解决的是类似问题:

有一个 R × C 的网格,一开始全是 0。每次操作给一个矩形区域整体加上 V,最后输出整个网格。

例如:

给左上角 (r1, c1) 到右下角 (r2, c2) 的整个矩形都加 V

如果直接模拟,需要枚举矩形里的每一个格子。矩形很大、操作很多时会非常慢。

二维差分的思想和一维完全一样:

不要真的修改整个矩形,只在矩形的边界上做标记。最后通过二维前缀和把这些标记扩散成最终结果。


从一维公式推广到二维

一维区间 [L, R] + V 的标记是:

diff[L] += V;
diff[R + 1] -= V;

它的意思是:

从 L 开始生效,从 R+1 开始失效

二维矩形有两个方向:行方向和列方向。

如果只写:

diff[r1][c1] += V;

那么做二维前缀和时,V 会影响从 (r1,c1) 开始往右下方的整个大矩形,而不只是我们想要的 [r1,c1]..[r2,c2]

所以我们需要在右边和下边把影响取消掉,再把右下角被重复取消的部分加回来。

四个角的公式是:

diff[r1][c1]         += V;  // 从左上角开始生效
diff[r1][c2 + 1]     -= V;  // 到右边界之后,取消横向影响
diff[r2 + 1][c1]     -= V;  // 到下边界之后,取消纵向影响
diff[r2 + 1][c2 + 1] += V;  // 右下角被取消了两次,加回来一次

这四个标记就是二维差分的核心。

2D Difference Array Four Corners


用图理解四角标记

假设我们想给这个矩形加 V

          c1        c2
          ↓         ↓
      .   .   .   .   .
r1 -> .  +V  +V  +V   .
      .  +V  +V  +V   .
r2 -> .  +V  +V  +V   .
      .   .   .   .   .

我们放四个标记:

左上角:      +V   让影响开始
右上角外侧:  -V   让影响不要继续向右
左下角外侧:  -V   让影响不要继续向下
右下角外侧:  +V   修正右下方被减了两次的问题

也就是:

(r1, c1)       放 +V
(r1, c2 + 1)   放 -V
(r2 + 1, c1)   放 -V
(r2 + 1, c2+1) 放 +V

完整例子:两个矩形更新

一个 3 × 3 网格,初始全是 0。

执行两次更新:

1) update(1, 1, 2, 2, +5)
2) update(2, 2, 3, 3, +3)

第 1 次更新:左上 2 × 2 加 5

diff[1][1] += 5;
diff[1][3] -= 5;
diff[3][1] -= 5;
diff[3][3] += 5;

第 2 次更新:右下 2 × 2 加 3

diff[2][2] += 3;
diff[2][4] -= 3;
diff[4][2] -= 3;
diff[4][4] += 3;

所有标记完成后,diff 大致是:

       c=1  c=2  c=3  c=4
r=1:    5    0   -5    0
r=2:    0    3    0   -3
r=3:   -5    0    5    0
r=4:    0   -3    0    3

现在对它做二维前缀和:

diff[r][c] += diff[r-1][c] + diff[r][c-1] - diff[r-1][c-1];

恢复出的最终网格是:

       c=1  c=2  c=3
r=1:    5    5    0
r=2:    5    8    3
r=3:    0    3    3

中间的 (2,2) 等于 8,因为它同时被 +5+3 两个矩形覆盖。


完整 C++ 实现

📄 C++ 完整代码
// 二维差分数组 — 每次矩形更新 O(1),最终重建 O(R × C)
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int R, C, M;
    cin >> R >> C >> M;

    // 多开一圈哨兵:当 r2=R 或 c2=C 时,会访问 r2+1 或 c2+1
    vector<vector<long long>> diff(R + 2, vector<long long>(C + 2, 0));

    // 第一步:每次 O(1) 标记一个矩形更新
    while (M--) {
        int r1, c1, r2, c2;
        long long v;
        cin >> r1 >> c1 >> r2 >> c2 >> v;

        diff[r1][c1]         += v;
        diff[r1][c2 + 1]     -= v;
        diff[r2 + 1][c1]     -= v;
        diff[r2 + 1][c2 + 1] += v;
    }

    // 第二步:二维前缀和重建最终网格
    for (int r = 1; r <= R; r++) {
        for (int c = 1; c <= C; c++) {
            diff[r][c] += diff[r - 1][c]
                        + diff[r][c - 1]
                        - diff[r - 1][c - 1];
        }
    }

    // 第三步:输出最终网格
    for (int r = 1; r <= R; r++) {
        for (int c = 1; c <= C; c++) {
            cout << diff[r][c];
            if (c < C) cout << " ";
        }
        cout << "\n";
    }

    return 0;
}

二维差分什么时候用?

二维差分适合:

很多次矩形区域加法,最后统一得到整个网格

常见题型包括:

  • 多次给矩形区域涂色,最后问每个格子的颜色或覆盖次数
  • 多个矩形广告牌叠加,统计最终亮度
  • 给地图上一块矩形区域增加高度、温度或权重
  • 离线处理大量矩形更新

二维差分常见错误

  1. 四角符号写反: 记住是 + - - +
  2. 数组开小: 要能访问 r2 + 1c2 + 1,所以至少开到 R + 2C + 2
  3. 忘记最后二维前缀和: 四角标记只是变化,不是最终网格。
  4. 重建公式写错: 应该是 上 + 左 - 左上,即:
diff[r][c] += diff[r-1][c] + diff[r][c-1] - diff[r-1][c-1];

一句话记忆:

矩形更新看四角:左上加,右上减,左下减,右下加;最后做二维前缀和。


3.2.8 USACO 示例:最大子数组和

题目(Kadane 算法的变体): 找连续子数组的最大和。

📄 C++ 完整代码
// Kadane 算法 — O(N) 时间,O(1) 空间
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;
    vector<int> A(n);
    for (int &x : A) cin >> x;

    // Kadane 算法:O(n)
    long long maxSum = LLONG_MIN;  // 最小 long long
    long long current = 0;

    for (int i = 0; i < n; i++) {
        current += A[i];
        maxSum = max(maxSum, current);
        if (current < 0) current = 0;  // ← 关键行:和为负时重新开始
    }

    cout << maxSum << "\n";

    return 0;
}

💡 核心思路: 为什么当 current 为负时重置为 0?因为负的前缀和只会损害之后的任何子数组。如果当前运行和是 -5,任何从头开始(和为 0)的子数组都比继续 -5 要好。

用前缀和的替代方案: 最大子数组和等于 P[j] - P[i-1] 对所有对 (i,j) 的最大值。对于每个 j,当 P[i-1] 最小时取得最大。追踪运行中最小前缀和!

// 替代方案:最小前缀技巧 — 同样 O(N)
long long maxSum = LLONG_MIN, minPrefix = 0, prefix = 0;
for (int x : A) {
    prefix += x;
    maxSum = max(maxSum, prefix - minPrefix);  // 到此处结束的最优和
    minPrefix = min(minPrefix, prefix);         // 追踪目前见过的最小前缀
    // ⚠️ 注意:minPrefix 的更新必须在 maxSum 之后。
    // 若提前更新 minPrefix,相当于允许空子数组(长度为0)参与比较,
    // 会导致结果在全负数组时错误地返回 0 而非最大负数。
}

3.2.8 USACO 真题训练:把区间问题变成前缀差

前缀和/差分在 USACO 中通常不是以「请你写前缀和」出现,而是藏在下面这些题面信号里:

题面信号首选技术判断依据
多次询问 [L,R] 内某类元素数量/总和前缀和原数组不变,查询很多
多次给 [L,R] 加值,最后统一询问差分数组更新很多,但不穿插查询
网格中多次询问矩形区域二维前缀和矩形求和可用容斥
网格中多次给矩形加值二维差分四角标记,最后重建

真题 1:Breed Counting(USACO 2015 December Silver)— 多类别前缀和

题目链接: USACO 2015 December Silver P3: Breed Counting
对应模式: 多个前缀和数组
难度定位: Silver 入门

题干解读

N 头奶牛排成一行,每头奶牛的品种是 1、2、3 之一。接下来有 Q 次查询,每次给出区间 [L,R],要求输出这个区间里三种品种各有多少头。

关键条件:

  • 队伍顺序固定,不会修改。
  • 查询次数很多,不能每次扫描 [L,R]
  • 品种只有 3 类,可以为每一类单独建前缀和。

思路分析

对每个品种 b 建一个前缀数组:

prefix[b][i] = 前 i 头奶牛中,品种 b 出现的次数

区间 [L,R] 中品种 b 的数量就是:

prefix[b][R] - prefix[b][L-1]

这和普通前缀和完全一样,只是把「数值求和」变成了「某个类别是否出现」。

CPP 完整代码

✅ 完整代码:Breed Counting
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // USACO 官方文件 I/O 可按需打开:
    // freopen("bcount.in", "r", stdin);
    // freopen("bcount.out", "w", stdout);

    int n, q;
    cin >> n >> q;

    vector<array<int, 4>> prefix(n + 1);  // 只使用下标 1..3

    for (int i = 1; i <= n; i++) {
        int breed;
        cin >> breed;
        prefix[i] = prefix[i - 1];        // 继承前 i-1 头的计数
        prefix[i][breed]++;               // 当前品种加 1
    }

    while (q--) {
        int l, r;
        cin >> l >> r;
        for (int b = 1; b <= 3; b++) {
            int count = prefix[r][b] - prefix[l - 1][b];
            cout << count << " \n"[b == 3];
        }
    }

    return 0;
}

复杂度: 预处理 O(N × 3),每次查询 O(3),总复杂度 O(N+Q),空间 O(3N)

易错点提醒

  1. prefix[i] = prefix[i-1] 漏掉。 只给当前品种加 1 会丢失之前所有计数。
  2. L-1 下标越界。 使用 1-indexed 前缀和时,prefix[0] 是哨兵,查询从 L=1 也安全。
  3. 输出格式错误。 每个查询输出 3 个数字,最后换行。
  4. 文件名混淆。 官方文件 I/O 是 bcount.in/out,不是 breed.in/out

拓展思考

如果品种数量从 3 变成 10^5,但每次只询问一种品种,可以改用 map<breed, vector<int>> 存每个品种出现位置,再用二分查找统计区间内出现次数。选择数据结构时始终看「类别数 × 查询模式」。


真题 2:Haybale Stacking(USACO 2012 January Bronze)— 区间加法的差分数组

题目链接: USACO 2012 January Bronze P2: Haybale Stacking
对应模式: 差分数组 + 最终重建
难度定位: Bronze/Silver 基础

题干解读

N 堆草,初始高度都是 0。接下来 K 条指令,每条指令给区间 [A,B] 内每一堆草加 1。所有操作完成后,输出草堆高度的中位数。

关键条件:

  • N 可到 10^6K 可到 25000
  • 每次直接遍历 [A,B] 可能达到 O(NK),太慢。
  • 题目只问所有操作完成后的结果,不需要中途查询。

思路分析

差分数组把一次区间加法变成两个端点标记:

diff[A] += 1
diff[B+1] -= 1

最后从左到右取前缀和,就能恢复每个位置最终高度。然后排序高度数组,取中位数。

CPP 完整代码

✅ 完整代码:Haybale Stacking
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    // USACO 官方文件 I/O 可按需打开:
    // freopen("stacking.in", "r", stdin);
    // freopen("stacking.out", "w", stdout);

    int n, k;
    cin >> n >> k;

    vector<int> diff(n + 2, 0);  // 需要写到 b+1,最大可能是 n+1

    for (int i = 0; i < k; i++) {
        int a, b;
        cin >> a >> b;
        diff[a] += 1;
        diff[b + 1] -= 1;
    }

    vector<int> height(n + 1);
    int current = 0;
    for (int i = 1; i <= n; i++) {
        current += diff[i];
        height[i] = current;
    }

    sort(height.begin() + 1, height.end());
    cout << height[(n + 1) / 2] << "\n";  // n 为奇数,中位数下标固定

    return 0;
}

复杂度: 区间更新 O(K),重建 O(N),排序 O(N log N),空间 O(N)

易错点提醒

  1. diff 数组开小。 必须开到 n+2,因为 B=n 时会写 diff[n+1]
  2. 忘记重建。 差分数组不是最终高度,必须再做一次前缀和。
  3. 中位数下标错。 本题 N 是奇数,用 1-indexed 高度数组时中位数是 (N+1)/2
  4. 误以为需要线段树。 没有中途查询,差分数组更简单也更快。

拓展思考

如果题目改成「每次区间加后立刻查询某个位置/区间」,差分数组就不够了,需要树状数组或线段树。这正是第 5.8 章和第 5.7 章要解决的问题。


⚠️ 第 3.2 章常见错误

  1. 区间查询差一:P[R] - P[L] 而不是 P[R] - P[L-1]。始终用小例子验证。
  2. 溢出: 大值的前缀和可能超过 int 范围(2×10^9)。即使元素是 int,前缀数组也要用 long long
  3. 二维查询公式: 忘了二维查询中的 +P[r1-1][c1-1] 项——非常容易疏忽。
  4. 差分数组大小: 声明 diff[n+1] 但需要 diff[n+2](因为要写入下标 r+1,可能是 n+1)。
  5. 1-indexed vs 0-indexed: 用 0-indexed 前缀和时,查询公式变为 P[R+1] - P[L]。在一道题内选定一种约定并坚持用。
  6. 二维差分数组大小: 声明 diff[R+1][C+1] 但需要 diff[R+2][C+2]——四角更新会写入 (r2+1, c2+1),必须在范围内。
  7. 二维差分重建顺序: 二维前缀和重建必须从左到右、从上到下处理单元格(与构建二维前缀和的顺序相同)。顺序混乱会产生错误结果。

本章总结

📌 核心要点

技术构建时间查询时间空间使用场景
一维前缀和O(N)O(1)O(N)一维数组的区间和
二维前缀和O(RC)O(1)O(RC)二维网格的矩形和
差分数组O(N+M)O(1)*O(N)区间加法更新
二维差分数组O(RC+M)O(1)*O(RC)二维网格上的矩形加法
Kadane 算法O(N)O(1)最大子数组和

*需要 O(N) 的重建遍历后才能读取所有值。

🧩 核心公式速查

操作公式备注
一维区间和P[R] - P[L-1]P[0] = 0 是哨兵值
二维矩形和P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1]容斥:减两次,加一次
差分数组更新diff[L] += V; diff[R+1] -= V;数组大小应为 N+2
二维差分更新diff[r1][c1]+=V; diff[r1][c2+1]-=V; diff[r2+1][c1]-=V; diff[r2+1][c2+1]+=V四角标记
从差分恢复对 diff 取前缀和(一维或二维)结果是最终数组

❓ 常见问题

Q1:前缀和和差分数组有什么关系?

A:它们是逆运算。对数组取前缀和得到前缀和数组;对前缀和数组取差分(相邻元素差)则恢复原数组。之后对这些标记取前缀和,+V 和 -V 在 [L,R] 外相互抵消,净效果恰好是给 [L,R] 加了 V。

Q2:什么时候用前缀和 vs 差分数组?

A:经验法则——看操作类型:

  • 多次区间求和查询 → 前缀和(预处理 O(N),查询 O(1)
  • 多次区间加减操作 → 差分数组(更新 O(1),最后恢复 O(N)
  • 两种操作交替出现时,需要更高级的数据结构(如第 5.7 章的线段树)

Q3:前缀和能处理动态修改吗?(数组元素改变)

A:不能。前缀和是一次性预处理,之后数组不能改变。如果元素被修改,用树状数组(BIT)线段树,它们支持单点更新和 O(log N) 的区间查询。

Q4:为什么 Kadane 算法有两个版本(current=0 vs minPrefix)?

A:两者本质相同,都是 O(N)。第一种(经典 Kadane)更直觉:当前子数组和变负时重新开始。第二种(最小前缀法)用前缀和思维:最大子数组 = max(P[j] - P[i]) = max(P[j]) - min(P[i])。按个人喜好选择。

Q5:二维前缀和的空间限制是什么?

A:若 R、C 都最大 10^4,P 数组需要 10^8 个 long long(约 800MB)——超出内存限制。一般 R×C ≤ 10^6~10^7 是安全的。更大的网格考虑压缩或离线处理。

🔗 与后续章节的联系

  • 第 3.4 章(双指针):滑动窗口也能做区间查询,但只适用于固定大小或单调移动的窗口;前缀和更通用
  • 第 3.3 章(排序与搜索):二分查找可以与前缀和结合——例如在前缀和数组上二分查找第一个 ≥ 目标值的位置
  • 第 5.7 章(线段树):解决前缀和无法处理的「动态更新 + 区间查询」问题
  • 第 6.1–6.3 章(动态规划):很多状态转移涉及区间和;前缀和是优化 DP 的重要工具
  • 差分数组的思想(「在起点 +V,在终点后 -V」)在扫描线算法、事件排序等高级技术中反复出现

练习题

题目 3.2.1 — 区间求和 🟢 简单 读取 N 个整数和 Q 个查询,每个查询给出 L 和 R,打印下标 L 到 R(1-indexed)的元素之和。

提示 构建前缀和数组 P,其中 P[i] = A[1]+...+A[i],每次查询回答 P[R] - P[L-1]。
✅ 完整题解

核心思路: O(N) 预计算前缀和,每次查询 O(1) 回答 P[R] - P[L-1]

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, q; cin >> n >> q;
    vector<long long> P(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        int x; cin >> x;
        P[i] = P[i-1] + x;
    }
    while (q--) {
        int l, r; cin >> l >> r;
        cout << P[r] - P[l-1] << "\n";
    }
}

复杂度: O(N + Q) —— 远好于朴素 O(N × Q)。


题目 3.2.2 — 区间加法,单点查询 🟢 简单 从 N 个零开始,处理 M 次操作:每次给 L 到 R 的所有位置加 V。所有操作后打印每个位置的值。

提示 对每次更新用 `diff[L]` += V,`diff[R+1]` -= V,然后对 diff 取前缀和。
✅ 完整题解

核心思路: 差分数组。每次区间加法只影响 diff 的 2 个位置,最终值通过前缀和得到。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n, m; cin >> n >> m;
    vector<long long> diff(n + 2, 0);
    while (m--) {
        int l, r; long long v; cin >> l >> r >> v;
        diff[l] += v;
        diff[r+1] -= v;
    }
    long long cur = 0;
    for (int i = 1; i <= n; i++) {
        cur += diff[i];
        cout << cur << " \n"[i == n];
    }
}

复杂度: O(N + M) —— 每次更新 O(1),最后扫描 O(N)。


题目 3.2.3 — 矩形求和 🟡 中等 读取 N×M 网格和 Q 个查询,每个查询给出 (r1,c1,r2,c2),打印子网格的和。

提示 二维前缀和。查询 = P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1]。
✅ 完整题解

核心思路: 二维前缀和。P[i][j] = 从 (1,1) 到 (i,j) 的矩形和,对任意矩形查询用容斥相减。

#include <bits/stdc++.h>
using namespace std;
int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, m, q; cin >> n >> m >> q;
    vector<vector<long long>> P(n+1, vector<long long>(m+1, 0));
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            int x; cin >> x;
            P[i][j] = x + P[i-1][j] + P[i][j-1] - P[i-1][j-1];
        }
    while (q--) {
        int r1, c1, r2, c2; cin >> r1 >> c1 >> r2 >> c2;
        cout << P[r2][c2] - P[r1-1][c2] - P[r2][c1-1] + P[r1-1][c1-1] << "\n";
    }
}

复杂度: O(N × M + Q)。


题目 3.2.4 — USACO 2016 January Bronze:割草 🔴 困难 FJ 沿一条路径割草,被访问超过一次的格子构成「双重割草」区域,统计至少被访问两次的格子数。

提示 模拟路径,在二维访问计数中标记格子,统计值 ≥ 2 的格子。
✅ 完整题解

核心思路: 直接模拟——不需要复杂数据结构。沿路径走,对每个访问的格子递增二维计数器。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n; cin >> n;
    map<pair<int,int>, int> cnt;
    int x = 0, y = 0; cnt[{x,y}]++;
    while (n--) {
        char dir; int steps; cin >> dir >> steps;
        int dx = (dir=='E') - (dir=='W');
        int dy = (dir=='N') - (dir=='S');
        while (steps--) {
            x += dx; y += dy;
            cnt[{x,y}]++;
        }
    }
    int doubleMowed = 0;
    for (auto& [pos, c] : cnt) if (c >= 2) doubleMowed++;
    cout << doubleMowed << "\n";
}

复杂度: O(总步数 × log),受 map 操作主导。


题目 3.2.5 — 二维区间加法 🟡 中等 N×M 网格(初始全零),Q 次操作每次给矩形 [r1,c1][r2,c2] 加 V,输出最终网格。

提示 二维差分数组:每次更新标记 4 个角,然后通过二维前缀和重建。
✅ 完整题解

核心思路: 二维差分数组。每次矩形更新只触及 4 个角。最终网格 = 差分数组的二维前缀和。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n, m, q; cin >> n >> m >> q;
    vector<vector<long long>> D(n+2, vector<long long>(m+2, 0));
    while (q--) {
        int r1, c1, r2, c2; long long v;
        cin >> r1 >> c1 >> r2 >> c2 >> v;
        D[r1][c1] += v;
        D[r1][c2+1] -= v;
        D[r2+1][c1] -= v;
        D[r2+1][c2+1] += v;
    }
    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];
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cout << D[i][j] << " \n"[j == m];
}

复杂度: O(Q + N × M)。


题目 3.2.6 — 最大子数组(Kadane 算法) 🟡 中等 读取 N 个整数(可能为负),找连续子数组的最大和。

提示 Kadane 算法。如果所有数都是负数,答案 = 最大的单个元素。
✅ 完整题解

核心思路: 在每个位置,要么开始新的子数组,要么延伸当前的。cur = max(A[i], cur + A[i]),追踪最优值。

#include <bits/stdc++.h>
using namespace std;
int main() {
    int n; cin >> n;
    long long best = LLONG_MIN, cur = 0;
    for (int i = 0; i < n; i++) {
        long long x; cin >> x;
        cur = max(x, cur + x);
        best = max(best, cur);
    }
    cout << best << "\n";
}

为什么当 cur+x < x 时从头开始? 因为负的运行和只会损害未来的项——丢弃它,从当前元素重新开始。

追踪 [-2, 1, -3, 4, -1, 2, 1, -5, 4]:

x=-2: cur=-2, best=-2
x=1:  cur=max(1,-2+1)=1, best=1
x=-3: cur=max(-3,1-3)=-2, best=1
x=4:  cur=max(4,-2+4)=4, best=4
x=-1: cur=max(-1,4-1)=3, best=4
x=2:  cur=5, best=5
x=1:  cur=6, best=6 ✓
x=-5: cur=1, best=6
x=4:  cur=5, best=6

复杂度: O(N) 时间,O(1) 空间。


差分数组专项练习

下面几道题专门训练差分数组。建议先判断题目属于哪一类:

  • 一维区间加法,最后输出数组:用一维差分。
  • 一维区间覆盖次数,最后统计答案:用一维差分 + 扫描统计。
  • 二维矩形加法或覆盖次数:用二维差分。
  • 有中途查询:普通差分通常不够,需要树状数组或线段树。

题目 3.2.7 — 区间加法后的最大值 🟢 简单

有一个长度为 N 的数组,初始全为 0。接下来有 M 次操作,每次给区间 [L,R] 内所有位置加上 V。所有操作结束后,输出数组中的最大值。

提示 不需要保存最终数组的每个值。用差分数组记录所有区间加法,最后从左到右做前缀和,同时维护最大值。
✅ 完整题解

核心思路: 每次区间加法只改两个位置:diff[L] += Vdiff[R+1] -= V。最后扫描时,当前前缀和就是当前位置的最终值。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    vector<long long> diff(n + 2, 0);

    while (m--) {
        int l, r;
        long long v;
        cin >> l >> r >> v;
        diff[l] += v;
        diff[r + 1] -= v;
    }

    long long cur = 0;
    long long best = LLONG_MIN;

    for (int i = 1; i <= n; i++) {
        cur += diff[i];
        best = max(best, cur);
    }

    cout << best << "\n";
    return 0;
}

复杂度: O(N + M) 时间,O(N) 空间。


题目 3.2.8 — 被覆盖至少 K 次的位置数 🟡 中等

数轴上有 N 个位置,编号 1..N。给出 M 个区间 [L,R],表示这个区间被覆盖一次。请统计最后有多少个位置被覆盖次数至少为 K

提示 每个区间覆盖一次等价于区间加 `1`。先用差分数组求出每个位置的覆盖次数,再统计 `>= K` 的位置数量。
✅ 完整题解

核心思路: 这题不是要求输出最终数组,而是要求统计满足条件的位置数。差分数组恢复时顺手统计即可。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k;
    cin >> n >> m >> k;

    vector<int> diff(n + 2, 0);

    while (m--) {
        int l, r;
        cin >> l >> r;
        diff[l] += 1;
        diff[r + 1] -= 1;
    }

    int cur = 0;
    int answer = 0;

    for (int i = 1; i <= n; i++) {
        cur += diff[i];
        if (cur >= k) answer++;
    }

    cout << answer << "\n";
    return 0;
}

复杂度: O(N + M) 时间,O(N) 空间。


题目 3.2.9 — 原数组上的区间加法 🟡 中等

给定一个长度为 N 的原数组 A。接下来有 M 次操作,每次给区间 [L,R] 内所有元素加上 V。所有操作结束后,输出最终数组。

提示 这题初始数组不是全零。可以只用差分数组记录“额外增加量”,最后把增加量加回原数组;也可以先把原数组转成差分数组后直接修改。
✅ 完整题解

核心思路: 最简单的写法是:diff 只记录所有操作带来的增量。最后扫描 diff 得到当前位置总共增加了多少,再加到 A[i] 上。

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m;
    cin >> n >> m;

    vector<long long> a(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    vector<long long> diff(n + 2, 0);

    while (m--) {
        int l, r;
        long long v;
        cin >> l >> r >> v;
        diff[l] += v;
        diff[r + 1] -= v;
    }

    long long add = 0;
    for (int i = 1; i <= n; i++) {
        add += diff[i];
        cout << a[i] + add << " \n"[i == n];
    }

    return 0;
}

复杂度: O(N + M) 时间,O(N) 空间。


题目 3.2.10 — 矩形覆盖次数 🟡 中等

有一个 R × C 的网格,初始每个格子的覆盖次数都是 0。给出 M 个矩形,每个矩形由左上角 (r1,c1) 和右下角 (r2,c2) 表示。每个矩形会让内部所有格子的覆盖次数加 1。请统计最后有多少个格子的覆盖次数至少为 K

提示 这是二维差分数组。每个矩形更新四个角:左上 `+1`,右上外侧 `-1`,左下外侧 `-1`,右下外侧 `+1`。最后做二维前缀和并统计答案。
✅ 完整题解

核心思路: 二维差分的四角标记是 + - - +。恢复时使用二维前缀和公式:D[r][c] += D[r-1][c] + D[r][c-1] - D[r-1][c-1]

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int R, C, M, K;
    cin >> R >> C >> M >> K;

    vector<vector<int>> diff(R + 2, vector<int>(C + 2, 0));

    while (M--) {
        int r1, c1, r2, c2;
        cin >> r1 >> c1 >> r2 >> c2;

        diff[r1][c1] += 1;
        diff[r1][c2 + 1] -= 1;
        diff[r2 + 1][c1] -= 1;
        diff[r2 + 1][c2 + 1] += 1;
    }

    int answer = 0;

    for (int r = 1; r <= R; r++) {
        for (int c = 1; c <= C; c++) {
            diff[r][c] += diff[r - 1][c]
                        + diff[r][c - 1]
                        - diff[r - 1][c - 1];
            if (diff[r][c] >= K) answer++;
        }
    }

    cout << answer << "\n";
    return 0;
}

复杂度: O(R × C + M) 时间,O(R × C) 空间。


题目 3.2.11 — 判断是否需要差分数组 🟡 中等

下面四种题目分别应该优先考虑什么数据结构或算法?

  1. 数组不变,很多次询问 [L,R] 的区间和。
  2. 很多次给 [L,R] 加值,所有操作结束后输出最终数组。
  3. 很多次给 [L,R] 加值,并且每次操作后立刻询问某个位置的值。
  4. 很多次给矩形区域加值,所有操作结束后统计每个格子的最终值。
提示 区分关键点:是查询还是修改?是一维还是二维?是否有“中途查询”?
✅ 完整题解
  1. 前缀和。 数组不变,多次区间和查询,预处理后每次 O(1)
  2. 一维差分数组。 多次区间加法,最后统一恢复结果。
  3. 树状数组或线段树。 更新和查询交替出现,普通差分数组无法直接处理中途查询。
  4. 二维差分数组。 矩形区域批量加值,最后统一重建网格。

这道题的重点不是写代码,而是学会从题面信号中判断该用哪种工具。


🏆 挑战题:奶牛与油漆桶 N×M 网格中有油漆桶,每个有一个正值。选择任意矩形子网格。得分 = (子网格最大值) - (边界格子之和)。找最优矩形。(N, M ≤ 500)

✅ 解题思路

朴素枚举所有 O(N²M²) 矩形对 500² 来说太慢。改进方案:

  1. 用二维前缀和实现 O(1) 求和查询
  2. 对子网格中的最大值:预处理二维稀疏表(或每行 RMQ)实现 O(1) 最大值查询
  3. 边界和 = 总和 - 内部和(均通过前缀和)

总计:O(N²M²) 枚举 × O(1) 每次查询 = 对 N,M ≤ 500 在时间限制内。