前缀和与差分
前缀和
前缀和:数列的前n项和
前缀和:如果要多次查询区间[l,r]的和,则可以考虑使用
\(S_0 = 0,S_i = S_{i-1} + a_i\)
于是有
实现了\(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)。
差分做法:
- 在差分数组
diff的l位置上+= val。 - 在差分数组
diff的r+1位置上-= val。
时间复杂度为O(1)。
为什么这样做是正确的?
我们来分析一下:
diff[l] += val:这会导致从a[l]开始的所有元素都增加val。因为a[l] = diff[0] + ... + diff[l],a[l+1] = a[l] + diff[l+1],以此类推。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] = 5diff[4] -= 3->diff[4] = -1
- 更新后的差分数组:
diff = [1, 5, 2, 2, -1]
现在,我们通过差分数组反推原数组,验证结果:
a[0] = diff[0] = 1a[1] = a[0] + diff[1] = 1 + 5 = 6a[2] = a[1] + diff[2] = 6 + 2 = 8a[3] = a[2] + diff[3] = 8 + 2 = 10a[4] = a[3] + diff[4] = 10 + (-1) = 9
最终数组 a 变为 [1, 6, 8, 10, 9],区间 [1, 3] 的每个元素确实都增加了 3。
三、算法流程
使用差分算法处理一系列区间更新,然后得到最终数组的步骤如下:
- 初始化:根据原始数组
a,计算出差分数组diff。 - 处理更新:对于每一个更新操作
(l, r, val),执行:diff[l] += val; if (r+1 < n) diff[r+1] -= val; // 注意边界 - 前缀和还原:所有更新操作处理完毕后,对差分数组
diff求一遍前缀和,即可得到最终的数组。
四、适用场景
差分算法适用于以下场景:
- 大量区间更新:当需要对数组进行多次、大范围的区间增减操作时。
- 离线处理:所有更新操作都已知,可以一次性处理完所有更新,最后再输出结果。
常见应用:
- 公交车上下车人数统计(区间
[上车点, 下车点]人数+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 很大,差分算法则能带来巨大的时间收益。

浙公网安备 33010602011771号