[特殊字符] C++ 算法8:前缀和与差分

📚 C++ 算法笔记:前缀和与差分

一、前缀和 (Prefix Sum)

1. 概念介绍

前缀和,顾名思义,就是求一个序列前 n 项的和。它是一种预处理思想,通过预先计算并存储好前缀和,可以将多次"区间求和"查询的时间复杂度从 O(n) 降低到 O(1),极大地提高了效率。

  • 定义 :对于一个数组 a,其前缀和数组 s 的第 is[i] 表示 a 数组中前 i 个元素的和。
    • s[i] = a[1] + a[2] + ... + a[i]
  • 核心公式 :求原数组 a 中区间 [l, r] 的和,可以通过前缀和数组 s 快速计算:
    • sum(l, r) = s[r] - s[l-1]

2. 案例引入:从暴力求解到前缀和优化

问题 :给定一个长度为 n 的整数序列,进行 m 次查询。每次查询输入一对 l, r,要求输出原序列中从第 l 个数到第 r 个数的和。

  • 暴力解法

    对于每次查询,都使用一个 for 循环从 l 遍历到 r 并累加求和。

    • 时间复杂度 :每次查询需要 O(r-l+1) 的时间,最坏情况下为 O(n)。进行 m 次查询,总时间复杂度为 O(n * m) 。当 nm 很大时(例如 10^5),这个复杂度很容易导致超时。
  • 前缀和优化

    我们可以先进行一次预处理,计算出整个数组的前缀和 s。之后,每次查询只需要做一次减法运算。

    • 时间复杂度 :预处理需要 O(n) 的时间,每次查询仅需 O(1)。总时间复杂度为 O(n + m)。效率提升显著。

3. 知识点详解

步骤一:预处理,构建前缀和数组

我们定义一个数组 s,其中 s[i] 存储 a[1]a[i]

复制代码
// 假设数组 a 和 s 已经定义,且下标从 1 开始
s[0] = 0; // 边界条件,方便计算
for (int i = 1; i <= n; i++) {
    s[i] = s[i-1] + a[i]; // 递推公式
}

这个过程的时间复杂度是 O(n)。

步骤二:进行区间查询

当需要查询区间 [l, r] 的和时,直接使用公式 s[r] - s[l-1]

复制代码
// 查询区间 [l, r] 的和
int query(int l, int r) {
    return s[r] - s[l-1];
}

这个过程的时间复杂度是 O(1)。

原理

为什么 s[r] - s[l-1] 就是区间 [l, r] 的和呢?

  • s[r] = a[1] + a[2] + ... + a[l-1] + a[l] + ... + a[r]
  • s[l-1] = a[1] + a[2] + ... + a[l-1]
  • 两式相减,s[r] - s[l-1],前面从 a[1]a[l-1] 的部分就被抵消了,
  • 剩下的正好是 a[l] + ... + a[r]

4. 例题与代码实现

题目 :输入一个长度为 n 的整数序列,进行 m 次区间和查询。

输入样例:

复制代码
5 3
2 1 3 6 4
1 2
1 3
2 4

输出样例:

C++ 代码实现:

复制代码
1#include <iostream>
2#include <cstdio>
3using namespace std;
4
5const int N = 100010; // 根据数据范围定义数组大小
6
7int n, m;
8int a[N], s[N]; // a为原数组,s为前缀和数组
9
10int main() {
11    // 1. 输入数据
12    scanf("%d%d", &n, &m);
13    for (int i = 1; i <= n; i++) {
14        scanf("%d", &a[i]);
15    }
16
17    // 2. 预处理:构建前缀和数组
18    for (int i = 1; i <= n; i++) {
19        s[i] = s[i-1] + a[i];
20    }
21
22    // 3. 处理 m 次查询
23    while (m--) {
24        int l, r;
25        scanf("%d%d", &l, &r);
26        // 4. 利用前缀和公式 O(1) 查询
27        printf("%d\n", s[r] - s[l-1]);
28    }
29
30    return 0;
31}

二、差分 (Difference)

1. 概念介绍

差分可以看作是前缀和的逆运算。如果说前缀和是为了快速求"区间和",那么差分就是为了快速进行"区间修改"。它通过构造一个差分数组,将对原数组一个区间的批量修改操作,转化为对差分数组上两个点的修改,从而将时间复杂度从 O(n) 降低到 O(1)。

  • 定义 :对于原数组 a,其差分数组 b 的第 ib[i] 定义为 a[i]a[i-1] 的差。
    • b[i] = a[i] - a[i-1] (其中 b[1] = a[1])
  • 关系 :原数组 a 是差分数组 b 的前缀和。即 a[i] = b[1] + b[2] + ... + b[i]

2. 案例引入:高效的区间修改

问题 :给定一个长度为 n 的数组 a,进行 m 次操作。每次操作给定 l, r, c,要求将数组 a 中区间 [l, r] 内的所有数都加上 c。最后输出修改后的整个数组。

  • 暴力解法

    对于每次操作,都使用一个 for 循环从 l 遍历到 r,给每个 a[i] 加上 c

    • 时间复杂度 :每次操作 O(n),m 次操作总复杂度为 O(n * m)
  • 差分优化

    我们构造 a 的差分数组 b。要将 a[l, r] 区间都加上 c,只需对 b 进行两步操作:b[l] += cb[r+1] -= c

    • 时间复杂度 :每次操作 O(1),m 次操作后,再通过一次 O(n) 的前缀和运算还原 a 数组。总复杂度为 O(n + m)

3. 知识点详解

步骤一:构造差分数组

复制代码
1// 假设数组 a 和 b 已经定义
2b[1] = a[1];
3for (int i = 2; i <= n; i++) {
4    b[i] = a[i] - a[i-1];
5}

步骤二:进行区间修改

要将 a 数组的 [l, r] 区间所有元素加上 c

  1. b[l] += c;:这会使 a[l] 及之后的所有元素都加上 c
  2. b[r+1] -= c;:这会使 a[r+1] 及之后的所有元素都减去 c,从而抵消了上一步对 r 之后元素的影响。

最终效果就是只有 a[l]a[r] 的元素增加了 c

原理

想象一条数轴,b[l] += c 相当于在 l 位置施加了一个 +c 的力,这个力会一直传递到数组末尾。为了在 r 位置停止这个力的作用,我们在 r+1 位置施加一个 -c 的力来抵消它。

步骤三:还原原数组

所有修改操作完成后,对差分数组 b 求前缀和,即可得到最终的原数组 a

复制代码
1for (int i = 1; i <= n; i++) {
2    a[i] = a[i-1] + b[i]; // 此时 a[i] 已被更新为最终值
3}

4. 例题与代码实现

题目 :(AcWing 797. 差分) 对数组进行 m 次区间加法操作,输出最终数组。

输入样例:

复制代码
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1

输出样例:

复制代码
3 4 5 3 4 2

C++ 代码实现:

复制代码
1#include <iostream>
2#include <cstdio>
3using namespace std;
4
5const int N = 100010;
6
7int n, m;
8int a[N], b[N]; // a为原数组,b为差分数组
9
10int main() {
11    scanf("%d%d", &n, &m);
12    
13    // 1. 输入原数组并构造差分数组
14    for (int i = 1; i <= n; i++) {
15        scanf("%d", &a[i]);
16        b[i] = a[i] - a[i-1]; 
17    }
18
19    // 2. 处理 m 次区间修改操作
20    while (m--) {
21        int l, r, c;
22        scanf("%d%d%d", &l, &r, &c);
23        b[l] += c;     // 区间起点 +c
24        b[r+1] -= c;   // 区间终点后一位 -c
25    }
26
27    // 3. 通过差分数组还原并输出最终的原数组
28    for (int i = 1; i <= n; i++) {
29        a[i] = a[i-1] + b[i]; // a[i] 此时存储了最终结果
30        printf("%d ", a[i]);
31    }
32
33    return 0;
34}

三、差分应用案例:海底高铁

这是一个利用差分思想解决实际问题的经典案例。

问题简述

n 个城市排成一排,有 n-1 段铁路连接相邻城市。你需要按顺序访问 m 个城市。对于每段铁路,你可以选择买纸质车票,也可以办一张IC卡。目标是求出完成所有行程的最小花费。

核心思路

  1. 问题转化 :首先需要知道每一段铁路会被经过多少次。这本质上就是一个"区间修改"问题。从城市 x 到城市 y,意味着 xy 之间的所有路段访问次数都 +1
  2. 差分应用 :我们不需要真的去遍历 xy 的每一段路。可以维护一个差分数组 t
    • 如果要从城市 x 走到 y (假设 x < y),我们只需执行 t[x]++t[y]--
    • 对所有行程都进行这样的标记后,最后对差分数组 t 求一次前缀和,t[i] 的值就代表了第 i 段铁路被经过的次数。
  3. 计算花费 :对于每一段铁路 i,比较两种方案的花费:
    • 买纸质票:a[i] * t[i] (单价 * 次数)
    • 办IC卡:b[i] * t[i] + c[i] (折扣单价 * 次数 + 工本费)
    • 取两者中的较小值累加到总花费 ans 中。

C++ 代码实现:

复制代码
1#include <iostream>
2#include <cstdio>
3#include <algorithm> // 用于 min 函数
4using namespace std;
5
6typedef long long ll; // 使用 long long 防止结果溢出
7
8int main() {
9    ll n, m, ans = 0;
10    scanf("%lld%lld", &n, &m);
11
12    ll p[m+1];      // 存储访问城市的顺序
13    ll t[n+1] = {}; // 差分数组,记录每段路的访问次数,初始化为0
14    ll a[n+1], b[n+1], c[n+1]; // a:票价, b:IC卡扣费, c:工本费
15
16    // 输入访问顺序
17    for (ll i = 1; i <= m; i++) {
18        scanf("%lld", &p[i]);
19    }
20    // 输入每段铁路的费用信息
21    for (ll i = 1; i <= n-1; i++) {
22        scanf("%lld%lld%lld", &a[i], &b[i], &c[i]);
23    }
24
25    // 1. 使用差分思想标记每段路的访问次数
26    for (ll i = 1; i <= m-1; i++) {
27        ll x = p[i], y = p[i+1];
28        if (x > y) swap(x, y); // 保证 x 是起点,y 是终点
29        
30        t[x]++;   // 从第 x 段路开始,次数+1
31        t[y]--;   // 到第 y 段路结束,次数-1 (差分标记)
32    }
33
34    // 2. 对差分数组求前缀和,得到每段路的实际访问次数
35    for (ll i = 1; i <= n; i++) {
36        t[i] += t[i-1];
37    }
38
39    // 3. 计算总花费
40    for (ll i = 1; i <= n-1; i++) {
41        // 比较两种方案,取最小值
42        ans += min(a[i] * t[i], b[i] * t[i] + c[i]);
43    }
44
45    printf("%lld", ans);
46    return 0;
47}