在算法实战中,数组子段问题(如区间求和、计数、最值等)是高频且核心的挑战。很多开发者面对这类问题本能地想到暴力解法,导致代码在规模数据面前崩溃。本文将揭示一套由前缀和、差分数组和HashMap组成的底层思维框架,结合实战案例与数学推导,展示如何高效秒杀复杂子段问题。
核心思想:预处理数组,存储前 i
个元素的和 prefix[i]
。任意区间 [L, R]
的和可瞬间计算为 prefix[R+1] - prefix[L]
,将 O(n) 查询优化至 O(1)。
数学本质:
设原数组为 A[0..n-1]
,前缀和数组 P
满足:
P[0] = 0
P[i] = A[0] + A[1] + ... + A[i-1] (i ≥ 1)
则区间和:
sum(L, R) = P[R+1] - P[L]
实战案例1:区间求和
问题:给定数组,多次查询 [L, R]
的和。
class PrefixSum:
def __init__(self, nums):
self.prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
self.prefix[i+1] = self.prefix[i] + nums[i]
def query_range(self, L, R):
return self.prefix[R+1] - self.prefix[L]
# 测试
nums = [1, 3, -2, 4, 5]
ps = PrefixSum(nums)
print(ps.query_range(1, 3)) # 输出: 3 + (-2) + 4 = 5
性能对比:
前缀和解决了求和问题,但如何高效寻找满足特定条件的子段(如“和为K”、“0和1数量相等”)?HashMap 通过存储前缀状态实现降维优化。
问题升级:和为K的子数组数量
问题:给定数组 nums
和整数 k
,计算和为 k
的连续子数组个数。
暴力陷阱:双重循环枚举所有子数组 → O(n²),n=10⁵ 时超时。
HashMap优化思路:
prefix_sum
prefix_sum - k
是否在 HashMap 中出现过k
数学推导:
设存在 j < i
使得:
prefix[i] - prefix[j] = k
⇒ prefix[j] = prefix[i] - k
因此,prefix[j]
的出现次数即是以 i
结尾的、和为 k
的子数组数量。
代码实现:
def subarray_sum(nums, k):
prefix_sum = 0
count = 0
sum_count = {0: 1} # 初始化:前缀和为0出现1次
for num in nums:
prefix_sum += num
# 查找是否存在前缀和等于 prefix_sum - k
count += sum_count.get(prefix_sum - k, 0)
# 更新当前前缀和出现次数
sum_count[prefix_sum] = sum_count.get(prefix_sum, 0) + 1
return count
# 测试
nums = [3, 4, 7, 2, -3, 1, 4, 2]
k = 7
print(subarray_sum(nums, k)) # 输出: 4 (子数组:[3,4], [7], [7,2,-3,1], [1,4,2])
复杂度:时间 O(n),空间 O(n) 为何高效:HashMap 将二维搜索(枚举起点终点)压缩为一维遍历,利用哈希查找 O(1) 特性。
当问题涉及频繁区间修改(如区间内所有元素加一个值)时,差分数组是比线段树、树状数组更轻量的解决方案。
核心思想:
差分数组 diff
满足:
diff[0] = arr[0]
diff[i] = arr[i] - arr[i-1] (i ≥ 1)
对原数组区间 [L, R]
加 val
的操作,转化为:
diff[L] += val
diff[R+1] -= val (若 R+1 在范围内)
最后通过前缀和还原修改后的数组。
数学证明:
还原操作 arr[i] = diff[0] + diff[1] + ... + diff[i]
本质是前缀和。
区间 [L, R]
加 val
后,对还原值的影响:
i < L
:无影响L ≤ i ≤ R
:累加了 val
i > R
:diff[R+1] -= val
抵消多余影响实战案例:航班预订统计
问题:有 n
个航班,预订记录 bookings = [start, end, seats]
,求每个航班的最终预订数。
def corp_flight_bookings(bookings, n):
diff = [0] * (n + 2) # 多开空间避免越界
for start, end, seats in bookings:
diff[start] += seats
diff[end + 1] -= seats # 关键操作
# 通过前缀和还原数组
res = [0] * n
res[0] = diff[1]
for i in range(1, n):
res[i] = res[i-1] + diff[i+1]
return res
# 测试
bookings = [[1, 2, 10], [2, 3, 20], [2, 5, 25]]
n = 5
print(corp_flight_bookings(bookings, n))
# 输出: [10, 55, 45, 25, 25]
性能优势:
复杂问题:元素和为目标值的子矩阵数量
问题:给定矩阵 matrix
和目标值 target
,计算元素和等于 target
的子矩阵数量。
暴力解法:枚举左上角和右下角 → O(n²m²) → 绝对超时。
降维策略:
target
的子数组”问题 → 使用HashMap!代码实现:
def num_submatrix_sum_target(matrix, target):
rows, cols = len(matrix), len(matrix[0])
count = 0
# 预处理列方向前缀和
col_prefix = [[0] * cols for _ in range(rows+1)]
for i in range(rows):
for j in range(cols):
col_prefix[i+1][j] = col_prefix[i][j] + matrix[i][j]
# 固定上下边界
for top in range(rows):
for bottom in range(top, rows):
# 当前上下边界间的列和 → 一维数组
col_sum = [0] * cols
for j in range(cols):
col_sum[j] = col_prefix[bottom+1][j] - col_prefix[top][j]
# 在一维数组上使用HashMap技巧
prefix_sum = 0
sum_map = {0: 1}
for s in col_sum:
prefix_sum += s
count += sum_map.get(prefix_sum - target, 0)
sum_map[prefix_sum] = sum_map.get(prefix_sum, 0) + 1
return count
# 测试
matrix = [[0,1,0],[1,1,1],[0,1,0]]
target = 0
print(num_submatrix_sum_target(matrix, target)) # 输出: 4
关键洞察:
n+1
,避免边界判断错误{0: 1}
,处理从0开始的子数组prefix_sum - k
的查找等价于寻找历史状态,将双重循环解耦。
前缀和、差分数组与HashMap的组合,构成了解决数组子段问题的“黄金三角”。掌握其底层思维,不仅能秒杀LeetCode高频题,更能在实际工程中(如时间序列分析、数据压缩、实时统计系统)游刃有余。记住:所有优化源于对冗余计算的识别与消除——这正是算法设计的精髓所在。