前缀和与差分:从入门到精通(Python版)
一、核心概念:用生活例子理解
1.1 前缀和:预支的智慧
想象你有一本存折,记录着每天的存款:
第1天:+100元
第2天:+50元
第3天:+200元
第4天:-30元
前缀和就是提前计算好"到第N天为止的总金额":
到第1天:100元
到第2天:150元
到第3天:350元
到第4天:320元
这样如果有人问"第2天到第4天的总收支",你只需用第4天的总和减去第1天的总和:320 - 100 = 220元。
1.2 差分:记账的艺术
现在换一种情况,银行经理说:
"给第2天到第4天每天加10元利息"
"给第1天到第3天每天减5元手续费"
差分就是聪明的记账方法:只在变化开始和结束的地方做标记,最后再统一计算。
二、一维前缀和深度解析
2.1 基础实现与原理
def build_prefix_sum(arr):
"""
构建前缀和数组
:param arr: 原始数组 [a0, a1, a2, ..., an-1]
:return: 前缀和数组 [0, a0, a0+a1, a0+a1+a2, ...]
"""
n = len(arr)
prefix = [0] * (n + 1)
for i in range(1, n + 1):
prefix[i] = prefix[i-1] + arr[i-1]
return prefix
def query_range_sum(prefix, l, r):
"""
查询区间和
:param prefix: 前缀和数组
:param l: 区间左端点(从0开始)
:param r: 区间右端点
:return: arr[l] + arr[l+1] + ... + arr[r]
"""
return prefix[r+1] - prefix[l]
# 示例
arr = [1, 3, 5, 7, 2, 4]
prefix = build_prefix_sum(arr)
print(f"原始数组: {arr}")
print(f"前缀和数组: {prefix}")
print(f"区间[1, 3]的和: {query_range_sum(prefix, 1, 3)}") # 输出15
2.2 高级应用:统计区间特征
def count_range_avg_above_threshold(arr, threshold):
"""
统计有多少个区间的平均值大于threshold
技巧:将每个元素减去threshold,问题转化为求区间和大于0的区间个数
"""
n = len(arr)
# 构建转换后的前缀和
transformed = [x - threshold for x in arr]
prefix = [0] * (n + 1)
for i in range(1, n + 1):
prefix[i] = prefix[i-1] + transformed[i-1]
# 使用单调栈优化查找(这里简化为O(n²)实现)
count = 0
for i in range(n):
for j in range(i, n):
if prefix[j+1] - prefix[i] > 0:
count += 1
return count
# 示例
arr = [2, 1, 3]
print(f"平均值大于1的区间个数: {count_range_avg_above_threshold(arr, 1)}")
三、一维差分深度解析
3.1 基础实现与原理
def build_diff_array(arr):
"""
构建差分数组
:param arr: 原始数组
:return: 差分数组 diff,其中diff[0]=arr[0], diff[i]=arr[i]-arr[i-1]
"""
n = len(arr)
diff = [0] * n
diff[0] = arr[0]
for i in range(1, n):
diff[i] = arr[i] - arr[i-1]
return diff
def range_add(diff, l, r, value):
"""
区间增加操作
:param diff: 差分数组
:param l: 区间左端点
:param r: 区间右端点
:param value: 增加的值
"""
diff[l] += value
if r + 1 < len(diff):
diff[r+1] -= value
def reconstruct_from_diff(diff):
"""
从差分数组重建原数组
"""
arr = [0] * len(diff)
arr[0] = diff[0]
for i in range(1, len(diff)):
arr[i] = arr[i-1] + diff[i]
return arr
# 示例
arr = [1, 3, 5, 7, 2, 4]
diff = build_diff_array(arr)
print(f"原始数组: {arr}")
print(f"差分数组: {diff}")
# 对区间[1, 3]每个元素加5
range_add(diff, 1, 3, 5)
new_arr = reconstruct_from_diff(diff)
print(f"修改后的数组: {new_arr}")
3.2 高级应用:多次区间操作优化
def multiple_range_operations(arr, operations):
"""
处理多次区间操作
:param arr: 原始数组
:param operations: 操作列表,每个操作是(l, r, value)
:return: 操作后的数组
"""
n = len(arr)
# 初始化差分数组(全0)
diff = [0] * (n + 1) # 多一位用于处理边界
# 先构建原始数组的差分表示
diff[0] = arr[0]
for i in range(1, n):
diff[i] = arr[i] - arr[i-1]
# 应用所有操作
for l, r, value in operations:
diff[l] += value
if r + 1 < len(diff):
diff[r+1] -= value
# 重建数组
result = [0] * n
result[0] = diff[0]
for i in range(1, n):
result[i] = result[i-1] + diff[i]
return result
# 示例
arr = [1, 3, 5, 7, 2, 4]
operations = [(1, 3, 5), (0, 2, -2), (4, 5, 3)]
result = multiple_range_operations(arr, operations)
print(f"多次操作后的结果: {result}")
四、二维前缀和与差分
4.1 二维前缀和
def build_2d_prefix(matrix):
"""
构建二维前缀和矩阵
"""
if not matrix or not matrix[0]:
return []
m, n = len(matrix), len(matrix[0])
# 前缀和矩阵,大小(m+1) x (n+1)
prefix = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
prefix[i][j] = (prefix[i-1][j] + prefix[i][j-1] -
prefix[i-1][j-1] + matrix[i-1][j-1])
return prefix
def query_2d_range(prefix, x1, y1, x2, y2):
"""
查询二维区间和
"""
return (prefix[x2+1][y2+1] - prefix[x1][y2+1] -
prefix[x2+1][y1] + prefix[x1][y1])
# 示例
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
prefix = build_2d_prefix(matrix)
print(f"矩阵左上角到右下角(1,1)到(2,2)的和: {query_2d_range(prefix, 1, 1, 2, 2)}")
4.2 二维差分
def build_2d_diff(matrix):
"""
构建二维差分矩阵
"""
m, n = len(matrix), len(matrix[0])
diff = [[0] * (n+2) for _ in range(m+2)] # 周围多一圈0处理边界
# 初始化差分矩阵
for i in range(m):
for j in range(n):
diff[i+1][j+1] = (matrix[i][j] -
(matrix[i-1][j] if i > 0 else 0) -
(matrix[i][j-1] if j > 0 else 0) +
(matrix[i-1][j-1] if i > 0 and j > 0 else 0))
return diff
def range_add_2d(diff, x1, y1, x2, y2, value):
"""
二维区间增加操作
"""
diff[x1][y1] += value
diff[x1][y2+1] -= value
diff[x2+1][y1] -= value
diff[x2+1][y2+1] += value
def reconstruct_from_2d_diff(diff, m, n):
"""
从二维差分重建原矩阵
"""
matrix = [[0] * n for _ in range(m)]
# 计算前缀和
for i in range(1, m+1):
for j in range(1, n+1):
diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1]
matrix[i-1][j-1] = diff[i][j]
return matrix
# 示例
matrix = [
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
]
diff = build_2d_diff(matrix)
# 在2x2区域(从(0,0)到(1,1))每个元素加2
range_add_2d(diff, 0, 0, 1, 1, 2)
result = reconstruct_from_2d_diff(diff, 3, 3)
print("二维区间增加后的结果:")
for row in result:
print(row)
五、实战应用:LeetCode经典题目
5.1 区域和检索 - 数组不可变 (LeetCode 303)
class NumArray:
def __init__(self, nums: List[int]):
self.prefix = [0] * (len(nums) + 1)
for i in range(1, len(nums) + 1):
self.prefix[i] = self.prefix[i-1] + nums[i-1]
def sumRange(self, left: int, right: int) -> int:
return self.prefix[right+1] - self.prefix[left]
5.2 二维区域和检索 - 矩阵不可变 (LeetCode 304)
class NumMatrix:
def __init__(self, matrix: List[List[int]]):
if not matrix or not matrix[0]:
m, n = 0, 0
else:
m, n = len(matrix), len(matrix[0])
self.prefix = [[0] * (n+1) for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
self.prefix[i][j] = (self.prefix[i-1][j] + self.prefix[i][j-1] -
self.prefix[i-1][j-1] + matrix[i-1][j-1])
def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
return (self.prefix[row2+1][col2+1] - self.prefix[row1][col2+1] -
self.prefix[row2+1][col1] + self.prefix[row1][col1])
5.3 航班预订统计 (LeetCode 1109)
def corpFlightBookings(bookings: List[List[int]], n: int) -> List[int]:
diff = [0] * (n + 2) # 多两位处理边界
for first, last, seats in bookings:
diff[first] += seats
if last + 1 <= n:
diff[last + 1] -= seats
# 重建答案
result = [0] * n
result[0] = diff[1]
for i in range(1, n):
result[i] = result[i-1] + diff[i+1]
return result
六、性能分析与高级优化
6.1 时间复杂度对比
操作类型 暴力法 前缀和/差分法
单次区间查询 O(n) O(1)
单次区间修改 O(n) O(1)
m次查询 O(mn) O(n + m)
m次修改 O(mn) O(n + m)
6.2 空间复杂度优化
对于某些问题,我们不需要存储完整的前缀和/差分数组:
def max_subarray_sum(arr):
"""
最大子数组和 - 空间优化版
只需要前一个前缀和值,不需要整个数组
"""
if not arr:
return 0
max_sum = arr[0]
min_prefix = min(0, arr[0])
current_prefix = arr[0]
for i in range(1, len(arr)):
current_prefix += arr[i]
max_sum = max(max_sum, current_prefix - min_prefix)
min_prefix = min(min_prefix, current_prefix)
return max_sum
七、总结与进阶学习路径
7.1 核心思想总结
1. 前缀和:空间换时间,预处理O(n),查询O(1)
2. 差分:延迟更新,批量操作O(1),最终重建O(n)
3. 维度扩展:从一维到二维,原理相同但计算更复杂
4. 应用场景:区间查询、区间修改、统计问题等
7.2 进阶学习建议
1. 树状数组 (Fenwick Tree):支持动态更新的前缀和
2. 线段树 (Segment Tree):支持多种区间操作
3. 稀疏表 (Sparse Table):静态区间最值查询
4. 莫队算法:离线处理区间查询问题
7.3 练习题目推荐
1. ⭐⭐ 简单:LeetCode 303, 304, 1109
2. ⭐⭐⭐ 中等:LeetCode 560, 1248, 1314
3. ⭐⭐⭐⭐ 困难:LeetCode 1074, 327
通过系统学习前缀和与差分,你不仅能解决许多区间操作问题,还能为学习更高级的数据结构打下坚实基础。记住核心思想:预处理优化查询,差分优化更新,这是算法优化中的常用技巧。