➕ 前缀和 — 区间求和的捷径
一次预处理,O(1)回答任意区间和,前缀和是区间问题的瑞士军刀
📖 前缀和的核心概念
前缀和(Prefix Sum)是一种预处理技巧,通过计算数组前 i 个元素的和,将任意区间 [left, right] 的求和从 O(n) 降为 O(1)。前缀和的思想可以推广到二维(矩阵区域和)和差分(区间修改),是解决区间问题的核心武器。
前缀和的核心公式:
原数组: a[0] a[1] a[2] a[3] a[4]
[3, -1, 2, 1, 5]
前缀和: prefix[0]=0 (哨兵)
prefix[1]=3
prefix[2]=3+(-1)=2
prefix[3]=2+2=4
prefix[4]=4+1=5
prefix[5]=5+5=10
区间和公式:
sum[left, right] = prefix[right+1] - prefix[left]
示例: sum[1,3] = prefix[4] - prefix[1] = 5 - 3 = 2
验证: -1 + 2 + 1 = 2 ✓
关键: prefix[0]=0 (哨兵),统一处理边界
prefix[i] = a[0] + a[1] + ... + a[i-1]
即 prefix[i] 是前 i 个元素的和
1. 前缀和的变体
一维前缀和:区间求和 O(1)。
构建 O(n),查询 O(1)。
适用:多次区间求和查询。
二维前缀和:矩阵区域和 O(1)。
sum(r1,c1,r2,c2) = P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]
容斥原理:加两次减一次。
前缀和 + 哈希表:统计满足条件的子数组数量。
关键转化:prefix[j] - prefix[i] = target → prefix[i] = prefix[j] - target
在哈希表中查找 prefix[j] - target 出现的次数。
2. 前缀和的通用模板
# 一维前缀和构建
n = len(nums)
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + nums[i]
# 区间查询 [left, right]
def range_sum(left, right):
return prefix[right + 1] - prefix[left]
🎯 题目一:区域和检索 - 数组不可变 (LC 303)
题目描述:给定一个整数数组 nums,处理多个请求,求区间 [left, right] 内所有元素的和。
示例: nums = [-2, 0, 3, -5, 2, -1]
prefix: [0, -2, -2, 1, -4, -2, -3]
↑ ↑ ↑ ↑ ↑ ↑ ↑
0 -2 -2 1 -4 -2 -3
| i=0 i=1 i=2 i=3 i=4 i=5
query(0,2) = prefix[3]-prefix[0] = 1-0 = 1
验证: -2+0+3 = 1 ✓
query(2,5) = prefix[6]-prefix[2] = -3-(-2) = -1
验证: 3+(-5)+2+(-1) = -1 ✓
思路分析
标准前缀和:
构造函数中预计算 prefix 数组 O(n)。
sumRange 查询 O(1)。
如果不用前缀和,每次查询 O(n),m 次查询 O(mn)。
时间复杂度:构建 O(n),查询 O(1)
代码实现
class NumArray:
def __init__(self, nums: List[int]):
self.prefix = [0] * (len(nums) + 1)
for i, num in enumerate(nums):
self.prefix[i + 1] = self.prefix[i] + num
def sumRange(self, left: int, right: int) -> int:
return self.prefix[right + 1] - self.prefix[left]
复杂度分析
构建:O(n) | 查询:O(1)
空间复杂度:O(n)
变体练习
- LC 304 二维区域和检索 - 矩阵不可变
- LC 307 区域和检索 - 数组可修改(树状数组/线段树)
🎯 题目二:和为 K 的子数组 (LC 560)
题目描述:给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。
示例: nums = [1, 1, 1], k = 2
前缀和: prefix = [0, 1, 2, 3]
暴力: 枚举所有子数组 O(n²)
[1,1] → 和2 ✓
[1,1] → 和2 ✓ (第二个1开始的)
前缀和+哈希:
遍历j, 查找 prefix[i] = prefix[j] - k
j=0: prefix=1, 查1-2=-1 → 次数0
j=1: prefix=2, 查2-2=0 → 次数1 (prefix[0]=0) → count=1
j=2: prefix=3, 查3-2=1 → 次数1 (prefix[1]=1) → count=2
结果: 2
思路分析
前缀和 + 哈希表:
关键转化:sum[i,j] = prefix[j+1] - prefix[i] = k
→ prefix[i] = prefix[j+1] - k
遍历到 j 时,只需要在哈希表中查找 prefix[j+1] - k 出现了多少次。
哈希表记录每个前缀和值出现的次数。
时间复杂度:O(n),空间复杂度:O(n)
代码实现
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
from collections import defaultdict
count_map = defaultdict(int)
count_map[0] = 1 # 哨兵:前缀和为0出现1次
prefix_sum = 0
count = 0
for num in nums:
prefix_sum += num
count += count_map[prefix_sum - k]
count_map[prefix_sum] += 1
return count
复杂度分析
时间复杂度:O(n) — 一次遍历
空间复杂度:O(n) — 哈希表
哨兵 count_map[0] = 1 不能忘记!它处理 prefix_sum 本身就等于 k 的情况。例如 nums=[1], k=1,prefix_sum=1,需要查找 1-1=0,如果 map 中没有 0 就漏掉了。
变体练习
- LC 523 连续的子数组和(和为 k 的倍数)
- LC 974 和可被 K 整除的子数组
🎯 题目三:除自身以外数组的乘积 (LC 238)
题目描述:给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。请不要使用除法,且在 O(n) 时间复杂度内完成。
示例: nums = [1, 2, 3, 4]
answer[i] = 左侧乘积 × 右侧乘积
左侧乘积 L:
L[0] = 1 (哨兵)
L[1] = 1 (= nums[0])
L[2] = 1×2 (= nums[0]×nums[1])
L[3] = 1×2×3 (= nums[0]×nums[1]×nums[2])
右侧乘积 R:
R[3] = 1 (哨兵)
R[2] = 4 (= nums[3])
R[1] = 4×3 (= nums[3]×nums[2])
R[0] = 4×3×2 (= nums[3]×nums[2]×nums[1])
answer:
[1×24, 1×12, 2×4, 6×1] = [24, 12, 8, 6]
思路分析
左右前缀乘积:
answer[i] = L[i] × R[i],其中:
L[i] = nums[0] × nums[1] × ... × nums[i-1](i 左侧所有元素之积)
R[i] = nums[i+1] × ... × nums[n-1](i 右侧所有元素之积)
空间优化:先算 L 存入 answer,再用一个变量从右往左累积 R。
时间复杂度:O(n),空间复杂度:O(1)(不含输出数组)
代码实现
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
answer = [1] * n
# 左侧乘积
left = 1
for i in range(n):
answer[i] = left
left *= nums[i]
# 右侧乘积
right = 1
for i in range(n - 1, -1, -1):
answer[i] *= right
right *= nums[i]
return answer
复杂度分析
时间复杂度:O(n) — 两次遍历
空间复杂度:O(1) — 不含输出数组
这道题的本质是"前缀积"和"后缀积"的结合。前缀和是加减法版本,这道题是乘法版本。核心思想相同:预处理一侧信息,O(1) 回答。
变体练习
- LC 135 分发糖果(左右两次遍历)
- LC 42 接雨水(左右最大值的前缀预处理)
成就解锁:前缀术士 — 掌握一维前缀和、前缀和+哈希、前缀积三大核心技巧
LeetCode AC验证:LC 303 Range Sum Query - Immutable ✅ | LC 560 Subarray Sum Equals K ✅ | LC 238 Product of Array Except Self ✅
📝 课后练习
- LC 304 二维区域和检索(二维前缀和)
- LC 523 连续的子数组和(前缀和+哈希)
- LC 974 和可被 K 整除的子数组
- LC 724 寻找数组的中心下标
- LC 1480 一维数组的动态和(前缀和练习)
🔑 本课要点回顾
- 前缀和:一次预处理 O(n),区间查询 O(1)
- 哨兵 prefix[0] = 0,统一边界处理
- 区间和:sum[left, right] = prefix[right+1] - prefix[left]
- 前缀和+哈希:统计满足条件的子数组数量
- 前缀积:左右两次遍历,O(1) 额外空间
- 二维前缀和:容斥原理,加两次减一次
思考题:差分数组是前缀和的逆运算——如果需要频繁对区间 [l, r] 加一个值 val,如何用差分数组 O(1) 完成修改?提示:diff[l] += val, diff[r+1] -= val,然后求前缀和还原。
🔬 差分数组 — 前缀和的逆运算
如果说前缀和是"求和"的加速器,那么差分数组就是"区间修改"的加速器。两者互为逆运算,配合使用威力巨大。
差分数组的原理:
原数组: [2, 3, 5, 1, 4]
差分数组: [2, 1, 2, -4, 3] (diff[0]=a[0], diff[i]=a[i]-a[i-1])
前缀和还原: prefix[0]=2, prefix[1]=3, prefix[2]=5, prefix[3]=1, prefix[4]=4
→ 还原出原数组 ✓
区间修改: 对 [1, 3] 加 5
直接修改差分数组:
diff[1] += 5 → [2, 6, 2, -4, 3]
diff[4] -= 5 → [2, 6, 2, -4, -2]
求前缀和还原:
[2, 8, 10, 6, 4] ← 原数组[1,3]区间都+5了!
O(1) 区间修改 + O(n) 还原
差分数组操作速查
# 构建差分数组
n = len(nums)
diff = [0] * n
diff[0] = nums[0]
for i in range(1, n):
diff[i] = nums[i] - nums[i - 1]
# 区间 [l, r] 加 val: O(1)
def range_add(diff, l, r, val):
diff[l] += val
if r + 1 < len(diff):
diff[r + 1] -= val
# 还原原数组: O(n)
def restore(diff):
result = [diff[0]]
for i in range(1, len(diff)):
result.append(result[-1] + diff[i])
return result
差分数组的典型应用:航班预订统计(LC 1109)、拼车(LC 1094)。当你看到"多次区间修改,最后查询"的问题时,考虑差分数组。
思考题:如果同时需要区间修改和区间查询,单靠前缀和或差分数组都不够,需要什么数据结构?提示:树状数组(BIT)或线段树,可以在 O(log n) 内同时支持修改和查询。