前缀和算法:从原理到 Python 实现

229 阅读6分钟

前缀和算法是一种在数组处理中广泛应用的优化技术,其核心思想是通过预先计算并存储中间结果,将区间和的查询时间从 O (n) 降至 O (1)。这种 “空间换时间” 的策略在处理多次区间和查询的场景中能显著提升效率,是编程面试和算法竞赛中的高频考点。

一、一维前缀和

1.1 问题引入

对于一个给定的数组nums,如果需要频繁查询 “区间[l, r]内所有元素的和”(其中l和r为数组下标,且0 ≤ l ≤ r < n),最直接的方法是每次查询时遍历区间累加,时间复杂度为 O (r-l+1)。当查询次数q较大时(如q=10^5),总时间复杂度会达到 O (qn),可能导致超时。

前缀和算法通过一次 O (n) 的预处理,将所有区间和查询优化为 O (1),总时间复杂度降至 O (n+q),大幅提升效率。

1.2 原理与定义

一维前缀和数组prefix的定义为:

  • prefix[0] = 0(哨兵元素,方便边界处理)
  • prefix[i]表示数组nums中前i个元素的和(即nums[0]到nums[i-1]的和)

数学表达式为:

prefix[i] = nums[0] + nums[1] + ... + nums[i-1]

基于此定义,区间[l, r](对应数组元素nums[l]到nums[r])的和可通过以下公式计算:

sum(l, r) = prefix[r+1] - prefix[l]

1.3 示例说明

以数组nums = [1, 2, 3, 4, 5]为例:

  • 前缀和数组prefix的计算过程:
    • prefix[0] = 0
    • prefix[1] = nums[0] = 1
    • prefix[2] = nums[0] + nums[1] = 1+2=3
    • prefix[3] = nums[0]+nums[1]+nums[2] = 1+2+3=6
    • prefix[4] = 1+2+3+4=10
    • prefix[5] = 1+2+3+4+5=15
  • 若查询区间[1, 3](即元素2,3,4)的和:
    • 根据公式:sum(1,3) = prefix[4] - prefix[1] = 10 - 1 = 9,与直接计算2+3+4=9结果一致。

1.4 实现步骤

(1)构建前缀和数组

  1. 初始化前缀和数组prefix,长度为n+1(n为原数组长度);
  1. 令prefix[0] = 0;
  1. 遍历原数组,通过递推公式prefix[i] = prefix[i-1] + nums[i-1]计算prefix[1..n]。

(2)区间和查询

对于区间[l, r],直接返回prefix[r+1] - prefix[l]。

1.5 Python 代码实现

def build_prefix(nums):
    """构建一维前缀和数组"""
    n = len(nums)
    prefix = [0] * (n + 1)
    for i in range(1, n + 1):
        prefix[i] = prefix[i - 1] + nums[i - 1]  # 累加原数组元素
    return prefix
def query_sum(prefix, l, r):
    """查询区间[l, r]的和(0 ≤ l ≤ r < len(nums))"""
    if l < 0 or r >= len(prefix) - 1 or l > r:
        return 0  # 处理非法输入
    return prefix[r + 1] - prefix[l]
# 示例
nums = [1, 2, 3, 4, 5]
prefix = build_prefix(nums)
print(query_sum(prefix, 1, 3))  # 输出:9(2+3+4)
print(query_sum(prefix, 0, 4))  # 输出:15(1+2+3+4+5)

二、二维前缀和

2.1 问题扩展

在二维数组(矩阵)中,若需要查询 “子矩阵(x1, y1)到(x2, y2)内所有元素的和”(其中(x1,y1)为左上角坐标,(x2,y2)为右下角坐标),普通方法的时间复杂度为 O ((x2-x1+1)*(y2-y1+1)),效率更低。此时二维前缀和可将查询优化为 O (1)。

2.2 原理与定义

二维前缀和数组prefix的定义为:

  • prefix[i][j]表示以(0,0)为左上角、(i-1,j-1)为右下角的子矩阵的和(即原矩阵中[0..i-1]行、[0..j-1]列的元素和)。

其构建公式基于容斥原理

prefix[i][j] = matrix[i-1][j-1] 
              + prefix[i-1][j]  # 上方子矩阵和
              + prefix[i][j-1]  # 左方子矩阵和
              - prefix[i-1][j-1]  # 减去重复计算的左上角子矩阵和

对于子矩阵(x1,y1)到(x2,y2)(坐标从 0 开始),其和的计算公式为:

sum = prefix[x2+1][y2+1] 
     - prefix[x1][y2+1]  # 减去上方多余区域
     - prefix[x2+1][y1]  # 减去左方多余区域
     + prefix[x1][y1]  # 加回重复减去的区域

2.3 示例说明

对于矩阵:

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

其二维前缀和数组prefix(4x4)的计算过程如下:

  • prefix[0][] = 0,prefix[][0] = 0(边界初始化)
  • prefix[1][1] = 1(仅包含 (0,0))
  • prefix[1][2] = 1+2 = 3(包含 (0,0)、(0,1))
  • prefix[2][2] = 1+2+4+5 = 12(包含 (0,0)-(1,1) 的子矩阵)

查询子矩阵(0,1)到(2,2)(即第二列到第三列、第一行到第三行)的和:

sum = prefix[3][3] - prefix[0][3] - prefix[3][1] + prefix[0][1]
        = 45 - 0 - 12 + 0 = 33

验证:2+3+5+6+8+9 = 33,结果正确。

2.4 Python 代码实现

def build_2d_prefix(matrix):
    """构建二维前缀和数组"""
    m = len(matrix)
    if m == 0:
        return []
    n = len(matrix[0])
    # 初始化(m+1)x(n+1)的前缀和数组,首行首列均为0
    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] = matrix[i-1][j-1] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]
    return prefix
def query_2d_sum(prefix, x1, y1, x2, y2):
    """查询子矩阵(x1,y1)到(x2,y2)的和(0 ≤ x1 ≤ x2 < m,0 ≤ y1 ≤ y2 < n)"""
    if not prefix:
        return 0
    m, n = len(prefix)-1, len(prefix[0])-1
    if x1 < 0 or x2 >= m or y1 < 0 or y2 >= n or x1 > x2 or y1 > y2:
        return 0  # 处理非法输入
    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_2d = build_2d_prefix(matrix)
print(query_2d_sum(prefix_2d, 0, 1, 2, 2))  # 输出:33(2+3+5+6+8+9)

三、应用场景与扩展

前缀和算法的核心是预处理区间和的中间结果,除了直接的区间和查询,还可用于解决以下问题:

  1. 子数组和等于 k 的数量(通过前缀和 + 哈希表优化);
  1. 二维区域和检索(如 LeetCode 304 题);
  1. 奇偶性前缀和(用于统计区间内奇偶元素的数量)。

在实际应用中,需注意前缀和数组的边界处理(如索引从 0 开始还是 1 开始)和数据溢出问题(当数组元素过大时,需使用 64 位整数类型)。

总结

前缀和算法通过一次预处理将区间和查询优化为常数时间,是 “空间换时间” 策略的经典案例。一维前缀和适用于线性数组,二维前缀和适用于矩阵,两者均遵循 “预处理→查询” 的两步流程。掌握前缀和算法不仅能提升代码效率,更能培养 “预计算中间结果” 的算法思维,为解决复杂问题奠定基础。