跳转至

451_前缀和与差分数组


一句话说明

前缀和用 O(1) 回答"区间求和"查询;差分数组用 O(1) 完成"区间加减"更新,两者都是用预处理换取查询速度的技巧。


核心知识点

白话理解

前缀和:事先统计"前 n 天的累计销量",之后查询"第 3 到第 7 天"的销量,只需要 总前7天 - 总前2天 一步搞定,而不是重新加 5 个数。

差分数组:要给数组第 l 到 r 位置的每个元素 +3,不用一个个加,而是在 l 位置打个"+3"标记,在 r+1 位置打个"-3"标记,最后对标记数组求前缀和就恢复了。


经典题目与解法

题目1:区域和检索(LeetCode 303,一维前缀和)

题意:给定整数数组,多次查询 [left, right] 区间内元素之和。

class NumArray:
    def __init__(self, nums):
        """
        预处理:计算前缀和数组
        prefix[i] = nums[0] + nums[1] + ... + nums[i-1]
        (多一位占位,prefix[0] = 0 方便统一处理左边界)
        """
        n = len(nums)
        self.prefix = [0] * (n + 1)        # 多一位,prefix[0]=0

        for i in range(n):
            self.prefix[i + 1] = self.prefix[i] + nums[i]  # 累加

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

# 测试
na = NumArray([-2, 0, 3, -5, 2, -1])
print(na.sum_range(0, 2))  # 1(-2+0+3)
print(na.sum_range(2, 5))  # -1(3-5+2-1)
print(na.sum_range(0, 5))  # -3(全部求和)

预处理时间:O(n)
查询时间:O(1)
空间复杂度:O(n)


题目2:二维区域和检索(LeetCode 304,二维前缀和)

题意:给定 m×n 矩阵,多次查询矩形区域 (r1,c1)(r2,c2) 的元素和。

class NumMatrix:
    def __init__(self, matrix):
        """
        二维前缀和:prefix[i][j] = matrix中(0,0)到(i-1,j-1)的矩形和
        容斥原理:大矩形 - 左边 - 上边 + 左上角(避免重复减)
        """
        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] = (
                    matrix[i-1][j-1]          # 当前格子
                    + self.prefix[i-1][j]     # 上方矩形
                    + self.prefix[i][j-1]     # 左方矩形
                    - self.prefix[i-1][j-1]   # 减去重复的左上角
                )

    def sum_region(self, r1, c1, r2, c2) -> int:
        """
        O(1) 查询矩形区域和
        目标矩形 = 大矩形 - 左边 - 上边 + 左上角
        """
        return (
            self.prefix[r2+1][c2+1]
            - self.prefix[r1][c2+1]
            - self.prefix[r2+1][c1]
            + self.prefix[r1][c1]
        )

# 测试
matrix = [
    [3, 0, 1, 4, 2],
    [5, 6, 3, 2, 1],
    [1, 2, 0, 1, 5],
    [4, 1, 0, 1, 7],
    [1, 0, 3, 0, 5]
]
nm = NumMatrix(matrix)
print(nm.sum_region(2, 1, 4, 3))  # 8(右下角4×4子矩阵的某区域)
print(nm.sum_region(1, 1, 2, 2))  # 11(6+3+2+0)

题目3:航班预订统计(LeetCode 1109,差分数组)

题意:航班 1 到 n,有多批预订 [first, last, seats],每批在 [first, last] 的所有航班预订 seats 个座位,求每个航班的最终预订量。

def corp_flight_bookings(bookings, n):
    """
    差分数组:不逐一更新每个位置,而是在端点打标记
    最后对差分数组求前缀和,还原真实值

    差分数组 diff[i] = nums[i] - nums[i-1]
    区间 [l, r] 加 val:diff[l] += val, diff[r+1] -= val
    """
    diff = [0] * (n + 2)                   # 多两位防越界

    for first, last, seats in bookings:
        diff[first] += seats               # 区间左端+seats
        diff[last + 1] -= seats            # 区间右端+1 处-seats

    # 对差分数组求前缀和,还原真实预订量
    result = []
    running_sum = 0
    for i in range(1, n + 1):
        running_sum += diff[i]             # 累加得到第i个航班的总量
        result.append(running_sum)

    return result

# 测试
print(corp_flight_bookings([[1,2,10],[2,3,20],[2,5,25]], 5))
# [10, 55, 45, 25, 25]
# 航班1: 10
# 航班2: 10+20+25=55
# 航班3: 20+25=45
# 航班4: 25
# 航班5: 25

时间复杂度:O(n + k) k 是预订批数
空间复杂度:O(n)


题目4:和为 k 的子数组(LeetCode 560,前缀和+哈希)

def subarray_sum(nums, k):
    """
    前缀和 + 哈希表
    子数组[i,j]的和 = prefix[j+1] - prefix[i]
    找 prefix[j+1] - prefix[i] = k,即找 prefix[j+1] - k 在之前出现过几次
    """
    count = 0
    prefix_sum = 0
    seen = {0: 1}                          # 前缀和→出现次数,初始化0出现1次

    for num in nums:
        prefix_sum += num                  # 更新当前前缀和

        # 找有多少个之前的前缀和等于 prefix_sum - k
        count += seen.get(prefix_sum - k, 0)

        seen[prefix_sum] = seen.get(prefix_sum, 0) + 1  # 记录当前前缀和

    return count

print(subarray_sum([1,1,1], 2))   # 2([1,1]出现两次)
print(subarray_sum([1,2,3], 3))   # 2([1,2]和[3])

面试技巧

  1. 区间求和必想前缀和:O(1) 查询,代价是 O(n) 预处理
  2. 区间批量更新想差分:O(1) 更新,代价是最后需要前缀和还原
  3. 前缀和+哈希是组合技:找"和为k的子数组"等价于找"两个前缀和的差等于k"
  4. 注意下标偏移prefix[i+1] = prefix[i] + nums[i],查询 [l,r] = prefix[r+1] - prefix[l]
  5. 差分数组的撤销diff[l] += val; diff[r+1] -= val 是核心两行

速查表

# 一维前缀和
n = len(nums)
prefix = [0] * (n + 1)
for i in range(n):
    prefix[i+1] = prefix[i] + nums[i]
# 查询 [l, r] 的和
range_sum = prefix[r+1] - prefix[l]

# 差分数组
diff = [0] * (n + 1)
# 区间 [l, r] 加 val
diff[l] += val
diff[r+1] -= val
# 还原
result = []
s = 0
for d in diff[:n]:
    s += d
    result.append(s)

# 前缀和 + 哈希(找和为k的子数组数量)
seen = {0: 1}
prefix_sum = count = 0
for num in nums:
    prefix_sum += num
    count += seen.get(prefix_sum - k, 0)
    seen[prefix_sum] = seen.get(prefix_sum, 0) + 1
场景技术复杂度
区间求和查询一维前缀和预处理O(n),查询O(1)
矩形求和查询二维前缀和预处理O(mn),查询O(1)
区间批量加减差分数组更新O(1),还原O(n)
和为k的子数组前缀和+哈希O(n)