前缀和与差分

前缀和

前缀和:数列的前n项和
前缀和:如果要多次查询区间[l,r]的和,则可以考虑使用

\(S_0 = 0,S_i = S_{i-1} + a_i\)

于是有

\[S([l, r]) = S_r - S_{l-1} \]

实现了\(O(n)\)预处理,\(O(1)\)查询


好的,我们来探讨 差分 (Difference) 算法。

差分是一种与前缀和相对应的算法,它主要用于快速处理数组的区间更新操作。如果说前缀和擅长“查询”,那么差分就擅长“修改”。


一、什么是差分?

对于一个给定的数组 a,我们可以定义其差分数组 diff 如下:

  • diff[0] = a[0]
  • diff[i] = a[i] - a[i-1] (对于 i > 0

简单来说,差分数组的第 i 个元素等于原数组第 i 个元素与第 i-1 个元素的差值。

示例

  • 原数组:a = [1, 3, 5, 7, 9]
  • 差分数组:diff = [1, 2, 2, 2, 2]

二、差分的核心作用:区间更新

差分的神奇之处在于,它可以将一个区间 [l, r] 的更新,转化为对差分数组两个位置的更新。

假设我们想在原数组 a 的区间 [l, r] 上,给每个元素都加上一个值 val

常规做法:遍历 a[l]a[r],每个元素都 += val。时间复杂度为 O(r-l+1)

差分做法

  1. 在差分数组 diffl 位置上 += val
  2. 在差分数组 diffr+1 位置上 -= val
    时间复杂度为 O(1)

为什么这样做是正确的?

我们来分析一下:

  1. diff[l] += val:这会导致从 a[l] 开始的所有元素都增加 val。因为 a[l] = diff[0] + ... + diff[l]a[l+1] = a[l] + diff[l+1],以此类推。
  2. diff[r+1] -= val:这会抵消从 a[r+1] 开始的所有元素的 val 增量。

两者结合,就实现了只在 [l, r] 区间内增加 val 的效果。

示例

  • 原数组:a = [1, 3, 5, 7, 9]
  • 差分数组:diff = [1, 2, 2, 2, 2]

假设我们想在区间 [1, 3] 上都 +3

  • 按照差分法:
    • diff[1] += 3 -> diff[1] = 5
    • diff[4] -= 3 -> diff[4] = -1
  • 更新后的差分数组:diff = [1, 5, 2, 2, -1]

现在,我们通过差分数组反推原数组,验证结果:

  • a[0] = diff[0] = 1
  • a[1] = a[0] + diff[1] = 1 + 5 = 6
  • a[2] = a[1] + diff[2] = 6 + 2 = 8
  • a[3] = a[2] + diff[3] = 8 + 2 = 10
  • a[4] = a[3] + diff[4] = 10 + (-1) = 9

最终数组 a 变为 [1, 6, 8, 10, 9],区间 [1, 3] 的每个元素确实都增加了 3。


三、算法流程

使用差分算法处理一系列区间更新,然后得到最终数组的步骤如下:

  1. 初始化:根据原始数组 a,计算出差分数组 diff
  2. 处理更新:对于每一个更新操作 (l, r, val),执行:
    diff[l] += val;
    if (r+1 < n) diff[r+1] -= val; // 注意边界
    
  3. 前缀和还原:所有更新操作处理完毕后,对差分数组 diff 求一遍前缀和,即可得到最终的数组。

四、适用场景

差分算法适用于以下场景:

  1. 大量区间更新:当需要对数组进行多次、大范围的区间增减操作时。
  2. 离线处理:所有更新操作都已知,可以一次性处理完所有更新,最后再输出结果。

常见应用:

  • 公交车上下车人数统计(区间 [上车点, 下车点] 人数 +1)。
  • 日程安排中,在一段时间 [start, end] 内添加或删除某个事件。
  • 图像处理中的区域填充。

五、代码示例

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> a = {1, 2, 3, 4, 5};
    int n = a.size();
    
    // 1. 初始化差分数组
    vector<int> diff(n, 0);
    diff[0] = a[0];
    for (int i = 1; i < n; ++i) {
        diff[i] = a[i] - a[i-1];
    }

    // 2. 处理区间更新:在 [1, 3] 区间内每个元素 +2
    int l = 1, r = 3, val = 2;
    diff[l] += val;
    if (r + 1 < n) {
        diff[r + 1] -= val;
    }
    
    // 3. 前缀和还原数组
    vector<int> res(n);
    res[0] = diff[0];
    for (int i = 1; i < n; ++i) {
        res[i] = res[i-1] + diff[i];
    }
    
    // 输出结果
    for (int x : res) {
        cout << x << " "; // 输出: 1 4 5 6 5
    }
    cout << endl;
    
    return 0;
}

六、总结

差分算法是一种高效处理区间更新的技巧。

  • 核心思想:将对区间的操作转化为对差分数组两个端点的操作。
  • 优点:单次区间更新的时间复杂度为 O(1),非常适合处理大量更新。
  • 缺点:需要离线处理,并且在最后需要 O(n) 的时间来还原数组。

它和前缀和算法一起,是处理数组问题的两个非常重要的工具,经常在算法竞赛中被用到。

差分

1. 空间换时间的体现

  • 空间开销

    • 差分算法需要额外维护一个差分数组 diff,其大小与原数组 a 相同。
    • 这意味着我们需要多消耗 O(n) 的存储空间。
  • 时间收益

    • 单次区间更新
      • 暴力方法:对区间 [l, r] 中的每个元素进行更新,时间复杂度为 O(r - l + 1)
      • 差分方法:只需更新差分数组中的两个位置 diff[l]diff[r+1],时间复杂度为 O(1)
    • 多次更新
      • 如果有 m 次区间更新操作,暴力方法的总时间复杂度可能高达 O(m * n)
      • 而差分方法的总时间复杂度为 O(m + n)m 次更新操作,加上最后 O(n) 的前缀和还原)。

结论:通过额外消耗 O(n) 的存储空间,差分算法将多次区间更新的时间复杂度从 O(m * n) 降低到了 O(m + n),这正是“空间换时间”的体现。

2. 适用场景

差分算法特别适合以下场景:

  • 大量区间更新:当 m 很大时,时间收益非常显著。
  • 离线处理:所有更新操作都已知,可以一次性处理完所有更新,最后再输出结果。

例如:

  • 公交车客流量统计:在 [上车点, 下车点] 区间内的乘客数 +1
  • 日程安排:在 [开始时间, 结束时间] 内添加一个事件。
  • 图像处理:对 [x1, y1, x2, y2] 矩形区域进行亮度调整。

3. 与前缀和的关系

差分和前缀和是一对互逆操作:

  • 前缀和:将差分数组 diff 进行前缀和运算,可以还原出原数组 a
  • 差分:将原数组 a 进行差分运算,可以得到差分数组 diff

它们的组合使用可以高效地解决很多数组问题:

  • 前缀和:快速查询区间和。
  • 差分:快速进行区间更新。

4. 总结

差分算法是空间换时间的典型应用

  • 空间换时间:通过维护一个差分数组,将多次区间更新的时间复杂度从 O(m * n) 降低到 O(m + n)
  • 适用场景:大量区间更新、离线处理。
  • 与前缀和的关系:互逆操作,常结合使用。

在实际应用中,是否使用差分算法需要权衡空间和时间的开销。如果 m 很小,暴力方法可能更简单;但如果 m 很大,差分算法则能带来巨大的时间收益。

posted @ 2025-11-16 11:59  EcSilvia  阅读(4)  评论(0)    收藏  举报