➕ 前缀和与差分详解

区间求和 O(1) · 区间加减 O(1) · 二维扩展 · 前缀和与哈希表

一句话总结

前缀和用于"频繁查询区间和":预处理 O(n) 后,任意 [l, r] 的和都能 O(1) 算出。差分是它的逆运算,用于"频繁对区间做加减":每次修改只需 O(1) 打两个标记,最后做一次前缀和还原。两者本质都是用预处理换查询速度。

📊

前缀和

pre[i] = 前 i 个数之和。sum(l, r) = pre[r+1] - pre[l]。

🏷️

差分

diff[l] += v, diff[r+1] -= v。还原后即为区间加 v 的结果。

🔲

二维前缀和

pre[i][j] 表示左上角矩形之和,容斥原理求子矩阵和。

🔑

前缀和 + 哈希表

把"和为 k 的子数组个数"从 O(n²) 降到 O(n)。

一、一维前缀和

1.1 构造与查询

原数组 nums = [2, 4, 1, 5, 3],查询 [1, 3] 的和 nums: 2 4 1 5 3 ← 绿色段 [1,3] = 4+1+5 = 10 pre(长度 n+1): 0 2 6 7 12 15 pre[i] = 前 i 个元素之和 sum(1, 3) = pre[4] - pre[1] = 12 - 2 = 10 (pre[0] = 0 是哨兵,让 l=0 时公式照样成立) 一次 O(n) 预处理,之后任意区间和都是 O(1) 若区间查询极多(如 10⁵ 次),从 O(n) 每次降到 O(1),差距是决定性的
class NumArray: def __init__(self, nums): self.pre = [0] * (len(nums) + 1) for i, x in enumerate(nums): self.pre[i + 1] = self.pre[i] + x # 递推构造 def sumRange(self, left, right): return self.pre[right + 1] - self.pre[left] # O(1) 查询 # 更简洁的写法(Python) from itertools import accumulate pre = [0] + list(accumulate(nums))
💡 为什么 pre 长度是 n+1?为了让 left = 0 时公式 pre[right+1] - pre[left] 依然成立(此时需要 pre[0] = 0)。这个"哨兵位"是前缀和最容易写错的地方。

二、一维差分

2.1 核心思想

如果要对数组区间 [l, r] 里每个元素都加 v,朴素做法是 O(r-l+1)。差分数组只需两个 O(1) 操作:

对 [1, 3] 每个元素 +2:只需 diff[1] += 2, diff[4] -= 2 原数组: 2 4 1 5 3 diff 数组: 2 +2 -3 +4 -2 0 diff[1] += 2 diff[4] -= 2(撤销影响) 还原(做前缀和): 2 6 3 7 3 [1,3] 各 +2,其余不变 ✓ m 次区间修改:O(m + n),而非 O(m × n) diff[i] = nums[i] - nums[i-1];对 diff 做前缀和即可还原 nums
def range_add(n, operations): # operations: [(l, r, v), ...] 表示对 [l, r] 每个元素 +v diff = [0] * (n + 1) for l, r, v in operations: diff[l] += v if r + 1 < n: diff[r + 1] -= v # r+1 越界则不需要(已到末尾) # 对 diff 做前缀和还原 res, cur = [], 0 for i in range(n): cur += diff[i] res.append(cur) return res # 经典题:1109 航班预订统计(每笔订单给区间 [i, j] 各加 seats 个座位) def corpFlightBookings(bookings, n): diff = [0] * (n + 1) for first, last, seats in bookings: diff[first - 1] += seats # 题目下标从 1 开始 diff[last] -= seats for i in range(1, n): diff[i] += diff[i - 1] # 就地做前缀和 return diff[:n]

三、二维前缀和

3.1 容斥原理

求子矩阵 (x1,y1) 到 (x2,y2) 的和 目标 = D A(左上) B(右上) C(左下) pre[i][j] = 左上角 (0,0)~(i-1,j-1) 的和 构造:pre[i][j] = matrix[i-1][j-1] + pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] 查询:D = pre[x2+1][y2+1] - pre[x1][y2+1] - pre[x2+1][y1] + pre[x1][y1] 减掉上方和左方,再把重复减掉的左上角加回来 —— 标准的容斥
class NumMatrix: def __init__(self, matrix): m, n = len(matrix), len(matrix[0]) self.pre = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m): for j in range(n): self.pre[i + 1][j + 1] = (matrix[i][j] + self.pre[i][j + 1] + self.pre[i + 1][j] - self.pre[i][j]) def sumRegion(self, row1, col1, row2, col2): return (self.pre[row2 + 1][col2 + 1] - self.pre[row1][col2 + 1] - self.pre[row2 + 1][col1] + self.pre[row1][col1])

四、前缀和 + 哈希表:和为 K 的子数组

4.1 从 O(n²) 到 O(n)

560. 和为 K 的子数组。暴力枚举所有区间是 O(n²)。利用前缀和:区间 [j+1, i] 的和 = pre[i] - pre[j]。我们要它等于 K,即 pre[j] == pre[i] - K。于是遍历到 i 时,只需查"之前出现过多少次前缀和等于 pre[i] - K"。

from collections import defaultdict def subarraySum(nums, k): count = defaultdict(int) count[0] = 1 # ★ 关键:前缀和为 0 出现过 1 次(空前缀) pre = 0 ans = 0 for x in nums: pre += x # 当前前缀和 ans += count[pre - k] # 之前有多少个 pre[j] == pre - k count[pre] += 1 # 记录当前前缀和 return ans # 时间 O(n),空间 O(n)
⚠️ 两个易错点:
① count[0] = 1 必须有——它代表"从第 0 个元素开始的子数组"这种情况;
② 必须先查询再插入。如果先 count[pre] += 1 再查 count[pre-k],当 k=0 时会把自己也算进去,导致多算。

4.2 同类题目

题目技巧
560 和为 K 的子数组前缀和 + 哈希表计数
974 和可被 K 整除的子数组记录 pre % K 的出现次数(注意负数取模)
523 连续的子数组和同余定理 + 记录最早出现的余数下标
525 连续数组(0/1 相等)把 0 当成 -1,转化为"和为 0 的最长子数组"
930 和相同的二元子数组同上,计数版
1248 优美子数组记录奇数个数的前缀计数

五、总结与选型

场景工具预处理单次操作
静态数组,多次查区间和前缀和O(n)O(1)
多次区间加减,最后一次性输出差分O(1) 每次还原 O(n)
边修改边查询(动态)线段树 / 树状数组O(n)O(log n)
矩阵多次查子矩阵和二维前缀和O(mn)O(1)
矩阵多次子区域加减二维差分O(1) 每次还原 O(mn)
求和为 K 的子数组个数前缀和 + 哈希表—总 O(n)
一句话记住:前缀和是"差分"的逆,差分是"前缀和"的逆。查询多用前缀和,修改多用差分。两者结合(先差分打标记,再前缀和还原)能 O(n) 完成任意次区间加减。
与滑动窗口的关系:《滑动窗口》一篇中提到"有负数时不能滑动窗口求最短和 ≥ target"——那种情况的正确解法就是前缀和 + 单调队列 / 二分。前缀和不要求元素非负,是更通用的区间和工具。