30 秒速览

  • 什么时候用:频繁查询区间和、频繁对区间做加减操作
  • 核心思想:预处理让后续操作 O(1)
  • 时间复杂度:预处理 O(n),查询/修改 O(1)
  • 空间复杂度:O(n)

一、直觉建立

银行余额查询

想象你是银行柜员,要频繁查询某段时间的交易总额。

笨办法:每次都从第一天加到最后一天,O(n)

聪明办法:提前算好"从开户到每一天的累计金额",然后用减法快速得到任意区间的总额,O(1)

区间 [L, R] 的和 = 累计到 R 的总额 - 累计到 L-1 的总额
                = prefix[R+1] - prefix[L]

前缀和的本质

前缀和 = 预计算 + 空间换时间

原数组:   nums  = [1, 2, 3, 4, 5]
前缀和:   prefix = [0, 1, 3, 6, 10, 15]
                   ↑  ↑  ↑  ↑   ↑   ↑
                   空  1  1+2 ...

区间 [1, 3] 的和 = prefix[4] - prefix[1] = 10 - 1 = 9
即 nums[1] + nums[2] + nums[3] = 2 + 3 + 4 = 9

二、前缀和模板

一维前缀和

def build_prefix(nums: list[int]) -> list[int]:
    """构建前缀和数组"""
    n = len(nums)
    prefix = [0] * (n + 1)  # prefix[0] = 0 表示空前缀
    for i in range(n):
        prefix[i + 1] = prefix[i] + nums[i]
    return prefix

def range_sum(prefix: list[int], left: int, right: int) -> int:
    """查询区间 [left, right] 的和,O(1)"""
    return prefix[right + 1] - prefix[left]

为什么用 n+1 长度?

prefix[0] = 0  (空前缀,方便计算从开头到某位置的和)
prefix[1] = nums[0]
prefix[2] = nums[0] + nums[1]
...
prefix[i] = nums[0] + nums[1] + ... + nums[i-1]

这样区间 [L, R] 的和 = prefix[R+1] - prefix[L]

二维前缀和

def build_prefix_2d(matrix: list[list[int]]) -> list[list[int]]:
    """构建二维前缀和"""
    m, n = len(matrix), len(matrix[0])
    prefix = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(m):
        for j in range(n):
            prefix[i+1][j+1] = (prefix[i][j+1] + prefix[i+1][j]
                               - prefix[i][j] + matrix[i][j])
    return prefix

def region_sum(prefix: list[list[int]], r1: int, c1: int, r2: int, c2: int) -> int:
    """查询矩阵区域 [r1,c1] 到 [r2,c2] 的和"""
    return (prefix[r2+1][c2+1] - prefix[r1][c2+1]
            - prefix[r2+1][c1] + prefix[r1][c1])

二维前缀和图解

要求区域 (r1,c1) 到 (r2,c2) 的和:

┌──────────────────────┐
│        │      c1     │
│   A    │     c2      │
│        │      B      │
├────────┼─────────────┤
│   r1   │  目标区域   │
│   r2   │     D       │
│        │      C      │
└──────────────────────┘

目标区域 = 总面积 - A - B - C + (A与B与C重叠部分)
        = prefix[r2+1][c2+1]
          - prefix[r1][c2+1]      (减去上方)
          - prefix[r2+1][c1]      (减去左方)
          + prefix[r1][c1]        (加回左上角多减的部分)

三、典型例题

例题 1:LC 303. 区域和检索 - 数组不可变

题目:给定一个整数数组 nums,处理以下类型的多个查询:计算索引 left 和 right 之间的 nums 元素的和(包含 left 和 right)。

代码

class NumArray:
    def __init__(self, nums: list[int]):
        n = len(nums)
        self.prefix = [0] * (n + 1)
        for i in range(n):
            self.prefix[i + 1] = self.prefix[i] + nums[i]

    def sumRange(self, left: int, right: int) -> int:
        return self.prefix[right + 1] - self.prefix[left]

复杂度:初始化 O(n),查询 O(1)


例题 2:LC 560. 和为 K 的子数组(重点)

题目:给你一个整数数组 nums 和一个整数 k,请你统计并返回该数组中和为 k 的连续子数组的个数。

示例

输入:nums = [1,1,1], k = 2
输出:2

思路分析

为什么不能用滑动窗口?

滑动窗口需要单调性:窗口扩大时,和单调增加。但如果数组有负数,窗口扩大时和可能减小!

前缀和 + 哈希表

设 prefix[i] 表示 nums[0..i-1] 的和
子数组 [j, i-1] 的和 = prefix[i] - prefix[j]

问题转化:找有多少对 (j, i) 使得 prefix[i] - prefix[j] = k
即:prefix[j] = prefix[i] - k

代码

from collections import defaultdict

def subarraySum(nums: list[int], k: int) -> int:
    count = defaultdict(int)
    count[0] = 1  # 空前缀,处理从开头到某位置恰好等于 k 的情况

    prefix_sum = 0
    result = 0

    for num in nums:
        prefix_sum += num
        # 找之前有多少个前缀和等于 prefix_sum - k
        result += count[prefix_sum - k]
        count[prefix_sum] += 1

    return result

图解

nums = [1, 1, 1], k = 2

遍历过程:
i=0: prefix_sum=1, 需要找 prefix_sum-k=-1, count[-1]=0, result=0
     count = {0:1, 1:1}

i=1: prefix_sum=2, 需要找 prefix_sum-k=0, count[0]=1, result=1
     count = {0:1, 1:1, 2:1}

i=2: prefix_sum=3, 需要找 prefix_sum-k=1, count[1]=1, result=2
     count = {0:1, 1:1, 2:1, 3:1}

最终: result=2

复杂度:时间 O(n),空间 O(n)


例题 3:LC 1248. 统计「优美子数组」

题目:给你一个整数数组 nums 和一个整数 k。如果某个连续子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。请返回优美子数组的数量。

思路

把奇数位置记录下来,问题转化为:在奇数位置数组中找区间。

def numberOfSubarrays(nums: list[int], k: int) -> int:
    # 记录所有奇数的位置(在原数组中的索引+1)
    odd_indices = [0]  # 前置一个 0,方便计算
    for i, num in enumerate(nums, 1):
        if num % 2 == 1:
            odd_indices.append(i)

    # 统计优美子数组数量
    result = 0
    for i in range(k, len(odd_indices)):
        # 奇数区间 [i-k+1, i]
        # 左边可选的偶数个数 = odd_indices[i-k+1] - odd_indices[i-k]
        # 右边可选的偶数个数 = odd_indices[i+1] - odd_indices[i](如果有)
        left = odd_indices[i - k + 1] - odd_indices[i - k]
        right = (odd_indices[i + 1] if i + 1 < len(odd_indices)
                 else len(nums) + 1) - odd_indices[i]
        result += left * right

    return result

例题 4:LC 304. 二维区域和检索

题目:给定一个二维矩阵 matrix,实现 NumMatrix 类,支持查询子矩形范围内元素的总和。

代码

class NumMatrix:
    def __init__(self, matrix: list[list[int]]):
        m, n = len(matrix), len(matrix[0])
        self.prefix = [[0] * (n + 1) for _ in range(m + 1)]

        for i in range(m):
            for j in range(n):
                self.prefix[i+1][j+1] = (self.prefix[i][j+1] +
                                         self.prefix[i+1][j] -
                                         self.prefix[i][j] +
                                         matrix[i][j])

    def sumRegion(self, r1: int, c1: int, r2: int, c2: int) -> int:
        return (self.prefix[r2+1][c2+1] - self.prefix[r1][c2+1] -
                self.prefix[r2+1][c1] + self.prefix[r1][c1])

复杂度:初始化 O(mn),查询 O(1)


例题 5:LC 238. 除自身以外数组的乘积

题目:给你一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。不能使用除法。

示例

输入: nums = [1,2,3,4]
输出: [24,12,8,6]

思路:前缀积 + 后缀积

对于位置 i:
answer[i] = (左边所有元素的乘积) × (右边所有元素的乘积)
          = prefix[i-1] × suffix[i+1]

代码

def productExceptSelf(nums: list[int]) -> list[int]:
    n = len(nums)
    answer = [1] * n

    # 计算左边的乘积
    prefix = 1
    for i in range(n):
        answer[i] = prefix
        prefix *= nums[i]

    # 计算右边的乘积,同时更新答案
    suffix = 1
    for i in range(n - 1, -1, -1):
        answer[i] *= suffix
        suffix *= nums[i]

    return answer

图解

nums = [1, 2, 3, 4]

第一遍(计算左边乘积):
i=0: answer[0] = 1
i=1: answer[1] = 1
i=2: answer[2] = 1*2 = 2
i=3: answer[3] = 1*2*3 = 6
answer = [1, 1, 2, 6]

第二遍(计算右边乘积):
i=3: answer[3] = 6 * 1 = 6
i=2: answer[2] = 2 * 4 = 8
i=1: answer[1] = 1 * 3*4 = 12
i=0: answer[0] = 1 * 2*3*4 = 24
answer = [24, 12, 8, 6]

复杂度:时间 O(n),空间 O(1)(输出数组不算额外空间)


四、差分数组

差分数组的本质

差分数组 = 前缀和的逆操作

  • 前缀和:从原数组计算前缀和,支持 O(1) 区间查询
  • 差分数组:从原数组计算差分,支持 O(1) 区间修改
原数组:    nums  = [1,  3,  6, 10, 15]
差分数组:  diff  = [1,  2,  3,  4,  5]
                   ↑   ↑   ↑   ↑   ↑
                  首  差值 差值 差值 差值

diff[0] = nums[0]
diff[i] = nums[i] - nums[i-1]  (i > 0)

还原: nums[i] = sum(diff[0..i])

差分数组模板

def build_diff(nums: list[int]) -> list[int]:
    """构建差分数组"""
    n = len(nums)
    diff = [0] * n
    diff[0] = nums[0]
    for i in range(1, n):
        diff[i] = nums[i] - nums[i - 1]
    return diff

def range_add(diff: list[int], left: int, right: int, val: int):
    """区间 [left, right] 加上 val,O(1)"""
    diff[left] += val
    if right + 1 < len(diff):
        diff[right + 1] -= val

def restore(diff: list[int]) -> list[int]:
    """从差分数组还原原数组"""
    result = []
    curr = 0
    for d in diff:
        curr += d
        result.append(curr)
    return result

为什么差分数组能 O(1) 区间修改?

对区间 [2, 4] 加上 3:

原数组:     [1,  3,  6, 10, 15]
期望结果:   [1,  3,  9, 13, 18]
                      ↑   ↑   ↑
                      +3  +3  +3

差分数组原: [1,  2,  3,  4,  5]
差分数组改: [1,  2,  6,  4,  2]
                   ↑           diff[2] += 3
                           ↑   diff[5] -= 3(超出范围则不需要)

还原:
i=0: 1
i=1: 1+2=3
i=2: 3+6=9   ← 从这里开始多了 3
i=3: 9+4=13
i=4: 13+2=15  ← 从这里开始恢复正常(因为 diff[5]-=3 抵消了)

结果: [1, 3, 9, 13, 15]... 等等,这里有问题

关键理解

  • diff[left] += val:从 left 位置开始,所有后续元素都多了 val
  • diff[right+1] -= val:从 right+1 位置开始,抵消之前加的 val

例题 6:LC 1109. 航班预订统计

题目:这里有 n 个航班,分别从 1 到 n 编号。有一份航班预订表 bookings,其中 bookings[i] = [firsti, lasti, seatsi] 表示在从 firsti 到 lasti 的每个航班上预订了 seatsi 个座位。返回一个长度为 n 的数组 answer,表示每个航班预订的座位总数。

示例

输入: bookings = [[1,2,10],[2,3,20]], n = 3
输出: [10, 55, 20]... 等等,让我重新计算
      航班1: 10
      航班2: 10 + 20 = 30
      航班3: 20
输出: [10, 30, 20]

代码

def corpFlightBookings(bookings: list[list[int]], n: int) -> list[int]:
    diff = [0] * (n + 1)

    # 构建差分数组
    for first, last, seats in bookings:
        diff[first] += seats
        if last + 1 <= n:
            diff[last + 1] -= seats

    # 前缀和还原答案
    answer = []
    curr = 0
    for i in range(1, n + 1):
        curr += diff[i]
        answer.append(curr)

    return answer

复杂度:时间 O(n + m),空间 O(n),m 是预订数量


例题 7:LC 1094. 拼车

题目:假设你是一位顺风车司机,车上最初有 capacity 个空座位。车只能向一个方向行驶。给定行程 trips,其中 trips[i] = [numPassengersi, fromi, toi] 表示第 i 次行程有 numPassengersi 名乘客,在 fromi 上车,在 toi 下车。判断是否能接送所有乘客。

代码

def carPooling(trips: list[list[int]], capacity: int) -> bool:
    # 找最远的位置
    max_loc = max(trip[2] for trip in trips)
    diff = [0] * (max_loc + 1)

    for passengers, start, end in trips:
        diff[start] += passengers
        diff[end] -= passengers  # 在 end 位置下车

    curr = 0
    for d in diff:
        curr += d
        if curr > capacity:
            return False

    return True

复杂度:时间 O(n + m),空间 O(m)


五、前缀和 + 哈希表 模式总结

适用场景:统计满足某条件的子数组个数

from collections import defaultdict

def count_subarrays(nums: list[int], condition) -> int:
    count = defaultdict(int)
    count[0] = 1  # 空前缀

    prefix = 0
    result = 0

    for num in nums:
        prefix += num  # 或其他累加方式

        # 根据条件查找需要的前缀和
        target = ...  # 根据 prefix 和 condition 计算
        result += count[target]

        count[prefix] += 1

    return result

常见变体

问题查找条件
和为 K 的子数组count[prefix - k]
和能被 K 整除的子数组count[prefix % k]
和为奇数的子数组count[prefix % 2]

六、复盘清单

触发信号

  • 频繁查询区间和(前缀和)
  • 频繁对区间做加减操作(差分数组)
  • 统计满足某条件的子数组个数(前缀和 + 哈希表)

边界条件

  • 空数组
  • 单元素
  • 负数(滑动窗口失效,用前缀和+哈希)
  • 前缀和溢出(Python 自动处理大整数)

常见错误

  • 前缀和数组长度搞错(应该是 n+1)
  • 区间公式下标错误([L,R] = prefix[R+1] - prefix[L])
  • 忘记初始化 count[0] = 1
  • 差分数组还原时忘记处理边界

七、题目清单

前缀和

题号题目难度考点
303区域和检索Easy一维前缀和
304二维区域和检索Medium二维前缀和
560和为 K 的子数组Medium前缀和+哈希
974和可被 K 整除的子数组Medium前缀和+哈希
1248统计优美子数组Medium前缀和变形
238除自身以外数组的乘积Medium前缀积

差分数组

题号题目难度考点
1109航班预订统计Medium差分数组
1094拼车Medium差分数组
370区间加法Medium差分数组