目标:掌握前缀和的核心思想,能够识别"区间和"类问题,熟练运用前缀和+哈希表解决子数组问题,并理解差分数组与前缀和的互逆关系。
一、核心思想:用预处理换取查询效率
1.1 本质
前缀和的本质是积分思想------将"区间求和"转化为"两点相减"。
给定数组 a = [a₁, a₂, ..., aₙ],定义前缀和数组:
Si=a1+a2+⋯+ai(S0=0)S_i = a_1 + a_2 + \cdots + a_i \quad (S_0 = 0)Si=a1+a2+⋯+ai(S0=0)
关键公式(所有前缀和问题的基石):
∑j=lraj=Sr−Sl−1\boxed{\sum_{j=l}^{r} a_j = S_r - S_{l-1}}j=l∑raj=Sr−Sl−1
1.2 时间复杂度重构
| 场景 | 暴力 | 前缀和优化 |
|---|---|---|
| 预处理 | O(1) | O(n) |
| 单次查询 | O(n) | O(1) |
| m 次查询 | O(m·n) | O(n + m) |
适用条件:静态数组(无修改)+ 查询次数远多于数组长度。
二、一维前缀和:模板与细节
2.1 标准模板(1-indexed)
python
def build_prefix(a: list[int]) -> list[int]:
"""
构建前缀和数组
下标从1开始,S[0] = 0,避免边界特判
"""
n = len(a)
S = [0] * (n + 1)
for i in range(1, n + 1):
S[i] = S[i - 1] + a[i - 1] # a是0-indexed
return S
def range_sum(S: list[int], l: int, r: int) -> int:
"""查询闭区间[l,r]的和,l,r为1-indexed"""
return S[r] - S[l - 1]
# 验证
a = [1, 2, 3, 4, 5]
S = build_prefix(a)
print(S) # [0, 1, 3, 6, 10, 15]
print(range_sum(S, 2, 4)) # 2+3+4 = 9
print(range_sum(S, 1, 5)) # 15
为什么用 1-indexed?
S[0] = 0让l=1时的公式S[r] - S[0]依然成立- 无需特判边界,代码更简洁,bug 更少
2.2 取模场景
python
mod = 10**9 + 7
def build_prefix_mod(a: list[int]) -> list[int]:
n = len(a)
S = [0] * (n + 1)
for i in range(1, n + 1):
S[i] = (S[i - 1] + a[i - 1]) % mod
return S
def range_sum_mod(S: list[int], l: int, r: int) -> int:
"""取模减法必须加mod防负数"""
return (S[r] - S[l - 1] + mod) % mod
陷阱 :(S[r] - S[l-1]) % mod 在 S[r] < S[l-1] 时产生负数。因为 S[r] 和 S[l-1] 都是模后的值,真实差可能是 mod + S[r] - S[l-1]。
三、前缀和 + 哈希表:子数组问题的黄金组合
3.1 核心问题:和为 K 的子数组个数
LeetCode 560
给定数组 nums 和整数 k,求和等于 k 的连续子数组个数。
暴力 O(n²):枚举所有子数组,求和判断。
优化 O(n) :
∑j=lraj=k⇔Sr−Sl−1=k⇔Sl−1=Sr−k\sum_{j=l}^{r} a_j = k \Leftrightarrow S_r - S_{l-1} = k \Leftrightarrow S_{l-1} = S_r - kj=l∑raj=k⇔Sr−Sl−1=k⇔Sl−1=Sr−k
对于每个位置 r,只需统计之前有多少个前缀和等于 S_r - k。
python
from collections import defaultdict
def subarray_sum(nums: list[int], k: int) -> int:
"""
和为 K 的子数组个数
时间:O(n),空间:O(n)
"""
count = defaultdict(int)
count[0] = 1 # S_0 = 0 出现1次(空前缀)
prefix = 0
ans = 0
for num in nums:
prefix += num
ans += count[prefix - k] # 之前有多少个 S = prefix - k
count[prefix] += 1 # 当前前缀和入表
return ans
# 验证
print(subarray_sum([1, 1, 1], 2)) # 2: [1,1], [1,1]
print(subarray_sum([1, 2, 3], 3)) # 2: [1,2], [3]
print(subarray_sum([1, -1, 0], 0)) # 3: [1,-1], [0], [1,-1,0]
执行过程可视化 (nums = [1,1,1], k = 2):
| 步骤 | num | prefix | 需要 count[prefix-2] | count 状态 | ans |
|---|---|---|---|---|---|
| 初始 | --- | 0 | --- | {0:1} |
0 |
| i=0 | 1 | 1 | count[-1]=0 | {0:1, 1:1} |
0 |
| i=1 | 1 | 2 | count[0]=1 ✅ | {0:1, 1:1, 2:1} |
1 |
| i=2 | 1 | 3 | count[1]=1 ✅ | {0:1, 1:1, 2:1, 3:1} |
2 |
3.2 变体:最长平衡子串
问题 :字符串中只有 L 和 Q,求最长的平衡子串(L 和 Q 数量相等)。
转化 :L → +1,Q → -1,问题变为"和为 0 的最长子数组"。
python
def longest_balanced(s: str) -> int:
"""
最长平衡子串:L和Q数量相等
时间:O(n),空间:O(n)
"""
diff = [1 if c == 'L' else -1 for c in s]
prefix = 0
first = {0: -1} # 前缀和值 → 第一次出现的下标
ans = 0
for i, d in enumerate(diff):
prefix += d
if prefix in first:
ans = max(ans, i - first[prefix])
else:
first[prefix] = i # 只记录第一次出现,保证最长
return ans
# 验证
print(longest_balanced("LLQQ")) # 4: 整个字符串
print(longest_balanced("LQLLQQ")) # 4: "LLQQ" (下标1-4)
print(longest_balanced("LLL")) # 0: 无法平衡
为什么记录第一次出现? 因为 i - first[prefix] 是最大跨度。后面再遇到相同 prefix,i 更大,差值也更大,但第一次出现的位置决定了最长距离。
四、二维前缀和:矩阵区间查询
4.1 构建原理(容斥原理)
y1 y2
┌─────┬─────┐
x1 │ A │ B │
├─────┼─────┤
x2 │ C │ D │ ← 目标区域
└─────┴─────┘
S[x2][y2] = A+B+C+D
S[x1-1][y2] = A+C
S[x2][y1-1] = A+B
S[x1-1][y1-1] = A
D = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]
4.2 完整代码
python
def build_2d_prefix(matrix: list[list[int]]) -> list[list[int]]:
"""
构建二维前缀和
时间:O(n·m),空间:O(n·m)
"""
if not matrix or not matrix[0]:
return [[]]
n, m = len(matrix), len(matrix[0])
S = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
S[i][j] = (S[i-1][j] + S[i][j-1]
- S[i-1][j-1] + matrix[i-1][j-1])
return S
def query_2d(S: list[list[int]],
x1: int, y1: int, x2: int, y2: int) -> int:
"""
查询子矩阵和,坐标为1-indexed闭区间
"""
return (S[x2][y2] - S[x1-1][y2]
- S[x2][y1-1] + S[x1-1][y1-1])
# 验证
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
S = build_2d_prefix(matrix)
print(query_2d(S, 1, 1, 2, 2)) # 1+2+4+5 = 12
print(query_2d(S, 2, 2, 3, 3)) # 5+6+8+9 = 28
五、差分数组:前缀和的逆运算
5.1 核心关系
| 操作 | 正向 | 逆向 |
|---|---|---|
| 数组 → 前缀和 | 积分(累加) | --- |
| 差分 → 数组 | --- | 微分(相邻差) |
关键性质:对差分数组求前缀和 = 原数组;对原数组求差分 = 差分数组。
5.2 区间修改的 O(1) 优化
场景 :对数组的某个区间 [l, r] 全部加 v,最后统一查询。
暴力:每次修改 O(r-l+1),m 次修改 O(m·n)。
差分优化:
d[l] += v(从 l 开始增加)d[r+1] -= v(从 r+1 恢复原值)- 最后对
d求前缀和还原
python
def range_add_diff(n: int, operations: list[tuple[int, int, int]]) -> list[int]:
"""
多次区间加,最后查询每个位置的值
operations: [(l, r, v), ...]
"""
d = [0] * (n + 2) # 多开2位,避免r+1越界
for l, r, v in operations:
d[l] += v
d[r + 1] -= v
# 前缀和还原
a = [0] * (n + 1)
a[1] = d[1]
for i in range(2, n + 1):
a[i] = a[i - 1] + d[i]
return a[1:]
# 验证
ops = [(1, 3, 2), (2, 4, 3), (1, 5, 1)]
# 位置1: 2+1=3, 位置2: 2+3+1=6, 位置3: 2+3+1=6, 位置4: 3+1=4, 位置5: 1
print(range_add_diff(5, ops)) # [3, 6, 6, 4, 1]
适用条件:多次区间修改,少量(或最后统一)查询。与前缀和恰好互补。
六、进阶:前缀和与双指针
6.1 和 ≤ K 的子数组个数(元素非负)
当数组元素全部非负时,子数组和具有单调性:右端点右移,和增大;左端点右移,和减小。
python
def count_subarrays_leq_k(nums: list[int], k: int) -> int:
"""
统计和 <= K 的子数组个数
前提:nums 中所有元素 >= 0
时间:O(n)
"""
n = len(nums)
S = build_prefix(nums) # S[i] 表示前i个元素的和
left = 0
ans = 0
for right in range(1, n + 1):
# 收缩左边界,直到区间和 <= k
while left < right and S[right] - S[left] > k:
left += 1
# 所有以 right 结尾,left..right-1 开始的子数组都满足
ans += right - left
return ans
# 验证
print(count_subarrays_leq_k([1, 2, 3, 4], 5)) # 6
# [1], [2], [3], [1,2], [2,3], [1,2,3] (和<=5)
前提破坏 :如果有负数,单调性不成立,需要用有序集合 或树状数组维护前缀和。
七、常见陷阱与 Checklist
7.1 错误清单
| 陷阱 | 后果 | 修复 |
|---|---|---|
| 下标混乱(0/1混用) | WA | 统一 1-indexed |
取模减法未 + mod |
负数 | (a - b + mod) % mod |
差分 r+1 越界 |
RE | 数组开 n+2 |
| 二维查询坐标顺序 | 算错区域 | 画图验证 x1≤x2, y1≤y2 |
哈希表未初始化 count[0]=1 |
漏解 | 空前缀 S_0=0 必须计入 |
7.2 快速决策树
问题是否涉及"区间和"或"子数组和"?
├── 是 → 数组是否静态(无修改)?
│ ├── 是 → 查询次数多?前缀和 O(1) 查询
│ └── 否 → 差分数组 O(1) 修改,最后还原
│
└── 是 → 是否求"和为 K 的子数组"?
├── 是 → 前缀和 + 哈希表,O(n)
└── 否 → 元素非负?双指针 O(n)
└── 有负数?树状数组/线段树 O(n log n)
八、与其他数据结构的对比
| 结构 | 预处理 | 单点修改 | 区间查询 | 最佳场景 |
|---|---|---|---|---|
| 前缀和 | O(n) | ❌ 不支持 | O(1) | 静态、查询极多 |
| 差分数组 | O(n) | O(1)(区间) | O(n)(还原) | 修改多、最后查 |
| 树状数组 | O(n) | O(log n) | O(log n) | 动态、两者均衡 |
| 线段树 | O(n) | O(log n) | O(log n) | 动态、功能复杂 |
九、核心心法
前缀和的精髓不在于公式,而在于三种思维习惯:
1. 预处理意识
看到"多次查询"立即想:能否 O(n) 预处理,O(1) 回答?
2. 转化能力
将"区间和"转化为"单点差值",将"子数组问题"转化为"两前缀和配对"。
3. 边界把控
下标从 1 开始,S[0]=0,取模减法 + mod。细节决定 AC。
判断标准:当你看到"子数组和为 K"时,能在 10 秒内想到哈希表;看到"矩阵区间求和"时,能瞬间画出容斥图------前缀和才真正成为你的本能。