➕ 前缀和 — 区间求和的捷径

一次预处理,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] = targetprefix[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)

变体练习

🎯 题目二:和为 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 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) 回答。

变体练习

成就解锁:前缀术士 — 掌握一维前缀和、前缀和+哈希、前缀积三大核心技巧
LeetCode AC验证:LC 303 Range Sum Query - Immutable ✅ | LC 560 Subarray Sum Equals K ✅ | LC 238 Product of Array Except Self ✅

📝 课后练习

  1. LC 304 二维区域和检索(二维前缀和)
  2. LC 523 连续的子数组和(前缀和+哈希)
  3. LC 974 和可被 K 整除的子数组
  4. LC 724 寻找数组的中心下标
  5. LC 1480 一维数组的动态和(前缀和练习)

🔑 本课要点回顾

📚 扩展阅读

思考题:差分数组是前缀和的逆运算——如果需要频繁对区间 [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) 内同时支持修改和查询。