NOI C++ 课程体系 / 算法篇 / 第8讲
CSP-J 入门级 · 算法策略 · m4-08

前缀和 与 差分

区间求和一次减法搞定,区间修改两端标记完成 —— 掌握竞赛中最高频的预处理技巧,将 O(n) 操作降为 O(1)!

6 大模块 1D+2D 前缀和 1D+2D 差分 3 个经典案例 20 道洛谷原题
本讲 · 第 32/56 讲前缀和与差分
模块 01

情景导航

用生活中的例子,直觉地理解前缀和与差分的本质。

故事超市库存管理

假设你是一家大型超市的店长,管理着 10000 个货架的商品库存。

老板突然问:"从第 3 号货架到第 15 号货架,今天总共增加了多少件商品?"

按照常规做法,你需要把 3 号到 15 号的每日变动量挨个加起来,要做 13 次加法。如果老板每天问几十次类似的问题,你会累疯!

有没有 O(1) 就能回答的方法?

聪明的做法(前缀和):每天晚上算一次"累计总增量"。当老板再问 3 到 15 号的增量时,你只需拿15号的累计值减去2号的累计值,一秒钟就能给出答案!

更聪明的做法(差分):如果老板说"把 5 号到 10 号货架全部增加 50 件",你也不用挨个改,只需在 5 号记一个"+50",在 11 号记一个"-50",最后统一结算即可。

前缀和 = 区间求和的利器 | 差分 = 区间修改的利器

💡 前缀和与差分的关系

前缀和:预处理一次 O(n),之后每次区间查询 O(1)。适合静态数组 + 多次查询的场景。

差分:每次区间修改 O(1),最后统一还原 O(n)。适合多次修改 + 最后查询的场景。

两者互为逆运算:对差分数组求前缀和 = 还原原数组;对原数组求差分 = 得到差分数组。

①生活类比
🚗

里程表 = 前缀和

记录从起点到当前的总路程,求两段距离只需两次读数相减。

→ s[r] - s[l-1]
💰

银行余额 = 前缀和

每天余额 = 历史总和,查某几天总进出 = 首尾余额相减。

→ 累计量
📦

快递签收单 = 差分

不直接改库存,只在区间起点记"+"、终点记"-",最后统一盘点。

→ 端点标记
🚇

地铁客流 = 差分

记录每站上下车人数差,累加差值即可还原每站实际人数。

→ 差分还原
②核心定义

前缀和 (Prefix Sum):数组 s 中 s[i] = a[1] + a[2] + ... + a[i],即原数组从第 1 项到第 i 项的累计和。

差分 (Difference):数组 d 中 d[i] = a[i] - a[i-1],即原数组相邻两项的差值。差分是前缀和的逆运算。

📌 本讲核心知识地图

板块核心内容对应大纲
一维前缀和s[i]=s[i-1]+a[i],查询 s[r]-s[l-1]前缀和
二维前缀和容斥原理,子矩阵 O(1) 查询前缀和
一维差分d[l]+=v, d[r+1]-=v,区间修改 O(1)差分
二维差分四个端点操作,子矩阵修改 O(1)差分
互逆关系前缀和 ↔ 差分,积分与微分算法策略
③核心口诀
🔑 前缀和

区间求和莫循环,前缀相减秒出答;
预处理 O(n),查询 O(1),空间换时间!

🔑 差分

区间修改别遍历,两端打标记就搞定;
d[l] 加 v,d[r+1] 减 v,最后前缀和还原!

模块 02

知识探究

深入掌握一维/二维前缀和与差分的公式推导、代码实现和经典应用。

1一维前缀和 核心

设原数组为 a[1..n],前缀和数组为 s[1..n]。

1
📐
定义

s[i] = a[1]+a[2]+...+a[i]

2
🔄
递推构建

s[i] = s[i-1] + a[i],s[0]=0

3
✅
区间查询

sum[l..r] = s[r] - s[l-1]

📊 一维前缀和可视化

原数组 a:

a[1]3
a[2]1
a[3]4
a[4]1
a[5]5
a[6]9

前缀和 s:

s[1]3
s[2]4
s[3]8
s[4]9
s[5]14
s[6]23
求 a[2..5] 的和 = s[5] - s[1] = 14 - 3 = 11 (即 1+4+1+5)
C++一维前缀和模板
#include <iostream>
using namespace std;
const int MAXN = 100005;
long long a[MAXN], s[MAXN];

int main() {
    int n, q; cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        s[i] = s[i-1] + a[i];  // 递推构建前缀和
    }
    while (q--) {
        int l, r; cin >> l >> r;
        cout << s[r] - s[l-1] << "\n";  // O(1) 查询
    }
    return 0;
}
操作暴力做法前缀和做法提升
预处理无O(n)一次性
单次区间查询O(r-l+1)O(1)快 n 倍
q 次查询总计O(q×n)O(n+q)巨大提升
2二维前缀和 高频

设原矩阵为 a[i][j],二维前缀和 s[i][j] 表示从左上角 (1,1) 到 (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] 的区域,加了两次,所以要减去一次。

子矩阵查询(左上(x1,y1)到右下(x2,y2)):

ans = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]

口诀:"大减小减再加回"

C++二维前缀和模板
const int MAXN = 1005;
long long a[MAXN][MAXN], s[MAXN][MAXN];

// 预处理
for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
        s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];

// 查询子矩阵 (x1,y1) 到 (x2,y2) 的和
long long ans = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
3一维差分 核心

差分是前缀和的逆运算。差分数组 d[i] = a[i] - a[i-1](a[0]=0)。对差分数组求前缀和即可还原原数组。

🔑 核心优势:区间修改 O(1)

对原数组 a 的区间 [l, r] 统一加上 v,只需在差分数组上操作两个点:

d[l] += v;   d[r+1] -= v;

原理:d[l] 加了 v 后,对 d 求前缀和时,从第 l 项开始每项都会多出 v。但第 r+1 项减了 v,从 r+1 项开始又抵消了。效果恰好是 [l, r] 区间加 v。

📊 一维差分可视化:对 [2,4] 区间加 3

原数组 a:[1, 2, 3, 4, 5] → 修改后:[1, 5, 6, 7, 5]

差分数组 d 的操作:

d[1]1
d[2]+3
d[3]0
d[4]0
d[5]-3
d[2] += 3(区间起点),d[5] -= 3(区间终点+1)→ 前缀和还原得 [1, 5, 6, 7, 5] ✓
C++一维差分模板
const int MAXN = 100005;
long long a[MAXN], d[MAXN];

// 构造差分数组
for (int i = 1; i <= n; i++)
    d[i] = a[i] - a[i-1];

// 区间 [l, r] 加 v —— O(1)
d[l] += v;
d[r+1] -= v;

// 所有操作完成后,还原原数组
for (int i = 1; i <= n; i++) {
    a[i] = a[i-1] + d[i];  // 对差分数组求前缀和
    cout << a[i] << " ";
}
操作暴力做法差分做法提升
单次区间修改O(r-l+1)O(1)快 n 倍
m 次修改 + 最终查询O(m×n)O(m+n)巨大提升
4二维差分 进阶

对二维矩阵的子矩阵 (x1,y1) 到 (x2,y2) 统一加上 v,在差分数组 d 上需要操作四个点:

📐 四个端点操作

d[x1][y1] += v;      // 左上角:开始加
d[x1][y2+1] -= v;    // 右上外:列方向停止
d[x2+1][y1] -= v;    // 左下外:行方向停止
d[x2+1][y2+1] += v; // 右下外:多加的要补回

记忆口诀:左上 +v,右上 -v,左下 -v,右下 +v("加减小加大")

C++二维差分模板
long long d[MAXN][MAXN];

// 子矩阵 (x1,y1)-(x2,y2) 加 v
d[x1][y1] += v;
d[x1][y2+1] -= v;
d[x2+1][y1] -= v;
d[x2+1][y2+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];
// 此时 d[i][j] 即为最终矩阵 a[i][j]
⚡前缀和与差分的互逆关系 辨析
📈 前缀和(积分/累加)
  • 对 a 求前缀和 → s
  • s[i] = s[i-1] + a[i]
  • 将单点值转化为累计值
  • 用途:区间查询 O(1)
📉 差分(微分/求差)
  • 对 a 求差分 → d
  • d[i] = a[i] - a[i-1]
  • 将区间操作转化为单点操作
  • 用途:区间修改 O(1)
操作适用技术预处理单次操作
多次区间求和(静态)前缀和O(n)O(1)
多次区间修改(最后查询)差分O(n)O(1)
二维子矩阵求和二维前缀和O(n×m)O(1)
二维子矩阵修改二维差分O(n×m)O(1)
区间修改 + 区间查询树状数组/线段树O(n)O(log n)
🔑 选用指南

• 只查询不修改 → 前缀和
• 只修改最后查 → 差分
• 边修改边查询 → 树状数组/线段树(进阶)

案例案例1:区间求和查询 经典

题目:给定 n 个整数和 q 次查询,每次查询 [l, r] 区间和。n, q ≤ 10⁵。

🔍 详细分析过程

第1步:识别模型 — 静态数组 + 多次区间求和 = 标准一维前缀和模型。

第2步:预处理 — 读入时构建前缀和数组 s[i] = s[i-1] + a[i],O(n)。

第3步:查询 — 每次查询输出 s[r] - s[l-1],O(1)。

第4步:注意数据类型 — n=10⁵,每个数最大10⁹,前缀和最大 10¹⁴,超出 int 范围,必须用 long long。

复杂度:预处理 O(n) + 查询 O(q) = O(n+q),完美通过。

C++区间求和 — 完整代码
#include <iostream>
using namespace std;
const int MAXN = 100005;
long long s[MAXN];
int main() {
    ios::sync_with_stdio(0);
    int n, q; cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        long long x; cin >> x;
        s[i] = s[i-1] + x;
    }
    while (q--) {
        int l, r; cin >> l >> r;
        cout << s[r] - s[l-1] << "\n";
    }
    return 0;
}
运行结果
输入: 5 3
      1 2 3 4 5
      1 5
      2 4
      3 3
输出: 15
      9
      3
案例案例2:区间加法更新 经典

题目:长度为 n 的全零数组,m 次操作 (l, r, v),最后输出数组。n, m ≤ 10⁵。

🔍 详细分析过程

第1步:识别模型 — 多次区间修改 + 最后一次性查询 = 差分模型。

第2步:初始化 — 全零数组的差分数组也是全零。d[1..n+1] = 0。

第3步:每次修改 — d[l] += v; d[r+1] -= v; 每次 O(1)。

第4步:还原输出 — a[i] = a[i-1] + d[i](前缀和还原),O(n)。

第5步:注意 — d 数组要开到 n+2,防止 r+1 越界。

C++区间加法更新 — 完整代码
#include <iostream>
using namespace std;
const int MAXN = 100005;
long long d[MAXN];
int main() {
    int n, m; cin >> n >> m;
    while (m--) {
        int l, r; long long v;
        cin >> l >> r >> v;
        d[l] += v;
        d[r+1] -= v;
    }
    long long cur = 0;
    for (int i = 1; i <= n; i++) {
        cur += d[i];  // 前缀和还原
        cout << cur << " ";
    }
    cout << endl;
    return 0;
}
运行结果
输入: 5 3
      1 3 2
      2 4 3
      3 5 1
输出: 2 5 6 4 1

💡 逐步验证

操作1 [1,3]+2:d[1]+=2, d[4]-=2

操作2 [2,4]+3:d[2]+=3, d[5]-=3

操作3 [3,5]+1:d[3]+=1, d[6]-=1

d = [0, 2, 3, 1, -2, -3, -1]

前缀和还原:2, 5, 6, 4, 1 ✓

案例案例3:二维子矩阵求和 NOIP

题目:给定 n×m 矩阵和 q 次查询,每次给出 (x1,y1,x2,y2),求子矩阵元素和。

🔍 详细分析过程

第1步:识别模型 — 静态二维矩阵 + 多次子矩阵求和 = 二维前缀和模型。

第2步:预处理 — 双重循环构建 s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j]。

第3步:查询 — 容斥公式 ans = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]。

第4步:边界 — 数组从下标 1 开始,s[0][*] = s[*][0] = 0,自然避免越界。

C++二维子矩阵求和 — 完整代码
#include <iostream>
using namespace std;
const int MAXN = 1005;
long long a[MAXN][MAXN], s[MAXN][MAXN];
int main() {
    ios::sync_with_stdio(0);
    int n, m, q; cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
            s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
        }
    while (q--) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        long long ans = s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
        cout << ans << "\n";
    }
    return 0;
}
运行结果
输入: 3 3 2
      1 2 3
      4 5 6
      7 8 9
      1 1 2 2
      2 2 3 3
输出: 12   (1+2+4+5)
      28   (5+6+8+9)
模块 03

实践应用

通过分析题和编程题,巩固前缀和与差分的应用能力。

题1前缀和查询

题目:给定数组 [3, 1, 4, 1, 5, 9, 2, 6],构建前缀和数组,并回答以下查询:① [1,4] ② [3,7] ③ [5,8]

查看参考答案

第1步:构建前缀和数组

a = [3, 1, 4, 1, 5, 9, 2, 6]

s[0]=0, s[1]=3, s[2]=4, s[3]=8, s[4]=9, s[5]=14, s[6]=23, s[7]=25, s[8]=31

第2步:回答查询

① [1,4] = s[4] - s[0] = 9 - 0 = 9(3+1+4+1)

② [3,7] = s[7] - s[2] = 25 - 4 = 21(4+1+5+9+2)

③ [5,8] = s[8] - s[4] = 31 - 9 = 22(5+9+2+6)

题2差分区间修改

题目:长度为 8 的全零数组,执行以下操作后输出最终数组:
操作1: [2,5] +3
操作2: [4,7] +2
操作3: [1,3] -1

查看参考答案

第1步:执行差分操作

操作1 [2,5]+3:d[2]+=3, d[6]-=3

操作2 [4,7]+2:d[4]+=2, d[8]-=2

操作3 [1,3]-1:d[1]+=-1, d[4]-=-1(即 d[4]+=1)

第2步:汇总差分数组

d = [0, -1, 3, 0, 3, 0, -3, 0, -2]

第3步:前缀和还原

a[1] = 0 + (-1) = -1

a[2] = -1 + 3 = 2

a[3] = 2 + 0 = 2

a[4] = 2 + 3 = 5

a[5] = 5 + 0 = 5

a[6] = 5 + (-3) = 2

a[7] = 2 + 0 = 2

a[8] = 2 + (-2) = 0

最终数组:[-1, 2, 2, 5, 5, 2, 2, 0]

题3综合:前缀和求子区间和等于 k 的个数

题目:给定数组 [1, 2, 3, -3, 1, 2],求有多少个子区间的和等于 3。

提示:子区间 [l,r] 的和 = s[r] - s[l-1] = k,即 s[l-1] = s[r] - k。
查看参考答案

第1步:构建前缀和

s[0]=0, s[1]=1, s[2]=3, s[3]=6, s[4]=3, s[5]=4, s[6]=6

第2步:对每个 s[r],查找 s[r]-3 在前面出现的次数

r=1: s[1]-3 = -2,未出现 → 0

r=2: s[2]-3 = 0,s[0]=0 出现1次 → +1 ✓ [1,2]=2+3=5? 不对,[1,2]=1+2=3 ✓

r=3: s[3]-3 = 3,前面 s[2]=3 出现1次 → +1 ✓ [3,3]=3

r=4: s[4]-3 = 0,s[0]=0 出现1次 → +1 ✓ [1,4]=1+2+3-3=3

r=5: s[5]-3 = 1,前面 s[1]=1 出现1次 → +1 ✓ [2,5]=2+3-3+1=3

r=6: s[6]-3 = 3,前面 s[2]=3,s[4]=3 出现2次 → +2 ✓

答案:1+1+1+1+2 = 6 个子区间

方法:用 map/unordered_map 记录每个前缀和出现的次数,边遍历边查找边累加。O(n) 复杂度。

#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
    int a[] = {1,2,3,-3,1,2};
    int n = 6, k = 3;
    unordered_map<int,int> cnt;
    cnt[0] = 1; // s[0] = 0
    long long s = 0, ans = 0;
    for (int i = 0; i < n; i++) {
        s += a[i];
        ans += cnt[s - k];
        cnt[s]++;
    }
    cout << ans << endl; // 6
}
模块 04

总结提升

1核心要点总结
知识板块必须记住的核心
一维前缀和s[i]=s[i-1]+a[i],查询 s[r]-s[l-1],O(1)
二维前缀和容斥原理构建,查询"大减小减再加回"
一维差分d[l]+=v, d[r+1]-=v,区间修改 O(1)
二维差分四端点操作:左上+,右上-,左下-,右下+
互逆关系前缀和 = 积分(累加),差分 = 微分(求差)
选用原则只查不改→前缀和,只改后查→差分
数据类型前缀和可能溢出 int,务必用 long long
2拓展:树状数组简介

💡 既要区间修改又要区间查询怎么办?

前缀和只擅长查询,差分只擅长修改。如果需要边修改边查询,就要用更高级的数据结构:

树状数组(BIT):用巧妙的二进制技巧,在 O(log n) 内完成单点修改和前缀和查询。

线段树:更通用的区间数据结构,支持任意区间修改和查询,O(log n)。

学习路线:先学好前缀和与差分(本讲)→ 再学树状数组 → 最后学线段树。

3思考与启发

💡 学完本讲,你应该掌握的6项能力

1. 能快速构建一维/二维前缀和数组。

2. 能用前缀和在 O(1) 内回答区间求和查询。

3. 能用差分实现 O(1) 的区间修改。

4. 能说出前缀和与差分的互逆关系。

5. 能判断一个问题应该用前缀和还是差分。

6. 能在二维场景下正确使用容斥原理。

模块 05

学习检测

5 个初学者最常犯的错误。

易错1

差分数组 d[r+1] 越界

❌ 错误
数组开 d[MAXN],当 r=n 时 d[n+1] 越界。
✅ 正确
数组开到 d[MAXN+5],确保 r+1 不越界。或者判断 if(r+1<=n) d[r+1]-=v。
口诀:差分数组多开几个,r+1 不怕越界!
易错2

前缀和 s[0] 忘记初始化

❌ 错误
s[0] 未设为 0,导致查询 [1,r] 时 s[0] 是垃圾值。
✅ 正确
s[0] = 0,全局数组自动初始化为 0。局部数组要手动设。
口诀:s[0]=0 不能忘,全局数组自动零!
易错3

数据类型溢出

❌ 错误
用 int 存前缀和,n=10⁵ 且每值 10⁹ 时,前缀和达 10¹⁴ 超出 int 范围。
✅ 正确
long long 存前缀和。max long long ≈ 9×10¹⁸,足够用。
口诀:前缀和用 long long,溢出问题不用愁!
易错4

二维容斥符号搞错

❌ 错误
查询时 s[x1-1][y1-1] 前面写成减号,结果少算了一块。
✅ 正确
公式是 s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1]。最后一个前是加号!
口诀:大减小减再加回,容斥原理记清楚!
易错5

差分还原顺序搞错

❌ 错误
从后往前还原,或者乱序累加。
✅ 正确
必须从前往后累加:a[i] = a[i-1] + d[i]。前缀和的方向不能反。
口诀:差分还原从前到后,a[i]=a[i-1]+d[i]!
速查前缀和与差分速查表
技术构建公式查询/修改复杂度
一维前缀和s[i]=s[i-1]+a[i]sum[l..r]=s[r]-s[l-1]建O(n) 查O(1)
二维前缀和s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j]容斥四角建O(nm) 查O(1)
一维差分d[i]=a[i]-a[i-1][l,r]+v: d[l]+=v, d[r+1]-=v改O(1) 还原O(n)
二维差分d[i][j]=a[i][j]-...四端点操作改O(1) 还原O(nm)
📌知识点笔记卡

📊 一维前缀和

s[i] = s[i-1] + a[i]

sum[l..r] = s[r] - s[l-1]

查询 O(1)

🧮 二维前缀和

容斥原理构建

大减小减再加回

子矩阵 O(1)

🔄 一维差分

d[l] += v

d[r+1] -= v

区间修改 O(1)

📐 二维差分

四端点操作

左上+右上-左下-右下+

子矩阵修改 O(1)

⚡ 互逆关系

前缀和 = 积分

差分 = 微分

互为逆运算

🎯 选用原则

只查不改 → 前缀和

只改后查 → 差分

边查边改 → 树状数组

课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 20 道洛谷原题。

🟢 A组 · 基础(4题)

A1编程题

一维前缀和:输入 n 个数和 q 次查询 (l, r),输出区间和。

查看参考答案
#include <iostream>
using namespace std;
long long s[100005];
int main() {
    int n, q; cin >> n >> q;
    for(int i=1;i<=n;i++){long long x;cin>>x;s[i]=s[i-1]+x;}
    while(q--){int l,r;cin>>l>>r;cout<<s[r]-s[l-1]<<endl;}
}

思路:先用 O(n) 预处理前缀和数组,之后每次区间查询直接由 s[r]−s[l−1] 得到答案。

易错:查询写成 s[r]−s[l](下标少减了 1),结果区间左端多算一个元素;前缀和数组没开 long long 时累加还会溢出。

A2编程题

一维差分:全零数组,m 次区间加操作,输出最终数组。

查看参考答案
#include <iostream>
using namespace std;
long long d[100005];
int main() {
    int n, m; cin >> n >> m;
    while(m--){int l,r;long long v;cin>>l>>r>>v;d[l]+=v;d[r+1]-=v;}
    long long cur=0;
    for(int i=1;i<=n;i++){cur+=d[i];cout<<cur<<" ";}
}

思路:每次区间加 v 只需改动差分数组两端:d[l]+=v、d[r+1]-=v,最后求一遍前缀和还原出原数组。

易错:把 d[r+1]-=v 写成 d[r]-=v,区间加变成了“从 l 一直加到末尾”;或数组没有多开一位导致 r+1 越界。

A3编程题

二维前缀和:n×m 矩阵,q 次子矩阵求和查询。

查看参考答案
#include <iostream>
using namespace std;
long long s[1005][1005];
int main() {
    int n,m,q;cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++){long long x;cin>>x;s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+x;}
    while(q--){int x1,y1,x2,y2;cin>>x1>>y1>>x2>>y2;
      cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;}
}

思路:用容斥公式 s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j] 构造,查询时对四个角做同样的容斥。

易错:构造公式里漏掉“+a[i][j]”这一项;或查询时把“−s[x1-1][y2-1]”误写成“+”,结果偏大。

A4编程题

求数组中有多少个子区间的和等于 k。

查看参考答案

思路:s[r]-s[l-1]=k → s[l-1]=s[r]-k。用 map 记录前缀和出现次数。

#include <iostream>
#include <unordered_map>
using namespace std;
int main() {
    int n; long long k; cin >> n >> k;
    unordered_map<long long,int> cnt;
    cnt[0] = 1;
    long long s = 0, ans = 0;
    for(int i=0;i<n;i++){long long x;cin>>x;s+=x;ans+=cnt[s-k];cnt[s]++;}
    cout << ans << endl;
}

易错:忘记初始化 cnt[0]=1,导致从下标 1 开始的那部分合法子区间全部漏掉。

🟡 B组 · 提高(4题)

B1差分应用

区间覆盖计数:n 个位置,m 个区间 [l,r] 各覆盖一次,求每个位置被覆盖的次数。

查看参考答案

分析:每个区间 [l,r] 覆盖一次 → d[l]++, d[r+1]--。最后前缀和还原即得每个位置的覆盖次数。

int d[MAXN];
while(m--){int l,r;cin>>l>>r;d[l]++;d[r+1]--;}
int cur=0;
for(int i=1;i<=n;i++){cur+=d[i];cout<<cur<<" ";}

易错:对每个位置逐区间累加统计覆盖次数(O(nm) 会超时);或差分数组没有多开一位,d[r+1]-- 时越界。

B2前缀和+数学

求数组中有多少个子区间的和能被 k 整除。

查看参考答案

分析:(s[r]-s[l-1])%k==0 → s[r]%k == s[l-1]%k。将前缀和对 k 取模,统计同余出现次数,C(cnt,2) 累加。

int cnt[MAXN]={}; cnt[0]=1;
long long s=0, ans=0;
for(int i=0;i<n;i++){
    s+=a[i];
    int r=(s%k+k)%k; // 处理负数取模
    ans+=cnt[r]; cnt[r]++;
}
cout<<ans;

易错:忘记处理负数前缀和的取模(C++ 中 −1%k 为负),必须写成 (s%k+k)%k;或漏掉 cnt[0]=1 的初始化。

B3二维差分

n×m 全零矩阵,q 次子矩阵加 v,输出最终矩阵。

查看参考答案
long long d[MAXN][MAXN]={};
while(q--){int x1,y1,x2,y2;long long v;cin>>x1>>y1>>x2>>y2>>v;
  d[x1][y1]+=v; d[x1][y2+1]-=v; d[x2+1][y1]-=v; d[x2+1][y2+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];
    cout<<d[i][j]<<" ";
  }

思路:二维差分的四个角分别做 +v、−v、−v、+v,还原时再用二维前缀和公式把影响扩散回整个矩阵。

易错:x2+1 或 y2+1 的下标超出数组范围;或四个角的加减符号写错,导致子矩阵之外的区域被误加。

B4前缀和最值

最大子段和:求数组中连续子数组的最大和。

查看参考答案

分析:max(s[r] - min_pre)。遍历 s[r] 时维护历史最小前缀和。

long long s=0, min_pre=0, ans=-1e18;
for(int i=0;i<n;i++){
    s+=a[i];
    ans=max(ans, s-min_pre);
    min_pre=min(min_pre, s);
}
cout<<ans;

易错:最小前缀和 min_pre 忘记初始化为 0,会漏掉从第一个元素开始的子段;或 ans 初值给 0,遇到全负数数组就答错。

🔴 C组 · 挑战(4题)

C1初赛真题

给定长度为 n 的数组 a 和前缀和数组 s,下列哪个表达式等于 a[l]+a[l+1]+...+a[r]?

A. s[r] - s[l]   B. s[r] - s[l-1]   C. s[r-1] - s[l]   D. s[r] + s[l-1]

查看参考答案

答案:B. s[r] - s[l-1]

s[r] = a[1]+...+a[r],s[l-1] = a[1]+...+a[l-1]。两者相减恰好等于 a[l]+...+a[r]。

考点:区间和等于两个前缀和之差,左端点要减到 l−1 而不是 l。

易错:把式子记成 s[r]−s[l](漏减 1),区间左边会多算一个元素 a[l]。

C2综合分析

环形数组最大子段和:数组首尾相连,求最大子段和。

查看参考答案

分析:两种情况取最大值:

① 不跨越尾部 = 普通最大子段和 max_sum

② 跨越尾部 = 总和 total - 最小子段和 min_sum

答案 = max(max_sum, total - min_sum)

特殊情况:如果所有元素都是负数,total - min_sum = 0(空子段),此时应返回 max_sum。

易错:直接对环形数组套用普通最大子段和,漏掉“跨越首尾”的情况;或全为负数时 total−min_sum 恰好为 0 而被当成合法答案。

C3综合编程

二维最大子矩阵和:求 n×m 矩阵中子矩阵的最大元素和。

查看参考答案

思路:枚举上下边界 i, j(O(n²)),将每列在 [i,j] 行的和压缩为一维数组,用一维最大子段和求解。总复杂度 O(n²×m)。

long long ans = -1e18;
for(int i=1;i<=n;i++)
  for(int j=i;j<=n;j++){
    long long s=0, mn=0;
    for(int k=1;k<=m;k++){
      s += col_sum[j][k] - col_sum[i-1][k]; // 第k列在[i,j]行的和
      ans = max(ans, s - mn);
      mn = min(mn, s);
    }
  }

易错:把算法写成枚举四个边界,复杂度 O(n²m²) 超时;或求列和时忘记用列前缀和,在循环里重复累加。

C4差分贪心

给定目标数组 a,初始全零,每次可选一个区间加 1 或减 1,求最少操作次数。

查看参考答案

分析:构造差分数组 d,每次操作等价于让 d[l] 和 d[r+1] 一个+1一个-1(或单独改一个端点)。

最少操作次数 = 差分数组中正数之和(也等于负数绝对值之和)。

long long pos=0;
for(int i=1;i<=n;i++){
    long long d = a[i] - a[i-1];
    if(d > 0) pos += d;
}
cout << pos << endl;

易错:把答案算成“差分数组中非零元素的个数”,或错误地取正负两部分之和的两倍——最少操作次数等于正数之和(也就等于负数绝对值之和)。

🏆 洛谷原题精选(20道)

以下 20 道洛谷题目均与前缀和/差分相关,按难度从入门到提高排列。

1
P1115
最大子段和入门
2
P1719
最大加权矩形入门
3
P3397
地毯(二维差分)入门
4
P1147
连续自然数和入门
5
P2004
领地选择普及-
6
P2367
语文成绩(差分入门)普及-
7
P1083
借教室(差分+二分)普及/提高-
8
P3406
海底高铁(差分/前缀和)普及-
9
P2879
区间统计普及-
10
P2216
理想的正方形普及+
11
P1880
石子合并(前缀和+DP)提高
12
P1351
联合权值(前缀和优化)普及+
13
P1955
程序自动分析(差分思想)提高
14
P3924
康娜的线段树(前缀和思想)提高
15
P2158
仪仗(前缀和+数论)普及+
16
P5020
货币系统(差分+背包)提高
17
P2671
推销员(前缀和+贪心)普及+
18
P2747
Matching Binary Strings(差分)提高
19
P1972
HH的项链(前缀和+离线)提高
20
P5838
Fruit(二维差分综合)提高