【每日一题】前缀和

目标:掌握前缀和的核心思想,能够识别"区间和"类问题,熟练运用前缀和+哈希表解决子数组问题,并理解差分数组与前缀和的互逆关系。


一、核心思想:用预处理换取查询效率

1.1 本质

前缀和的本质是积分思想------将"区间求和"转化为"两点相减"。

给定数组 a = [a₁, a₂, ..., aₙ],定义前缀和数组:

Si=a1+a2+⋯+ai(S0=0)S_i = a_1 + a_2 + \cdots + a_i \quad (S_0 = 0)Si=a1+a2+⋯+ai(S0=0)

关键公式(所有前缀和问题的基石):

∑j=lraj=Sr−Sl−1\boxed{\sum_{j=l}^{r} a_j = S_r - S_{l-1}}j=l∑raj=Sr−Sl−1

1.2 时间复杂度重构

场景 暴力 前缀和优化
预处理 O(1) O(n)
单次查询 O(n) O(1)
m 次查询 O(m·n) O(n + m)

适用条件:静态数组(无修改)+ 查询次数远多于数组长度。


二、一维前缀和:模板与细节

2.1 标准模板(1-indexed)

python 复制代码
def build_prefix(a: list[int]) -> list[int]:
    """
    构建前缀和数组
    下标从1开始,S[0] = 0,避免边界特判
    """
    n = len(a)
    S = [0] * (n + 1)
    for i in range(1, n + 1):
        S[i] = S[i - 1] + a[i - 1]  # a是0-indexed
    return S

def range_sum(S: list[int], l: int, r: int) -> int:
    """查询闭区间[l,r]的和,l,r为1-indexed"""
    return S[r] - S[l - 1]


# 验证
a = [1, 2, 3, 4, 5]
S = build_prefix(a)
print(S)                    # [0, 1, 3, 6, 10, 15]
print(range_sum(S, 2, 4))   # 2+3+4 = 9
print(range_sum(S, 1, 5))   # 15

为什么用 1-indexed?

  • S[0] = 0l=1 时的公式 S[r] - S[0] 依然成立
  • 无需特判边界,代码更简洁,bug 更少

2.2 取模场景

python 复制代码
mod = 10**9 + 7

def build_prefix_mod(a: list[int]) -> list[int]:
    n = len(a)
    S = [0] * (n + 1)
    for i in range(1, n + 1):
        S[i] = (S[i - 1] + a[i - 1]) % mod
    return S

def range_sum_mod(S: list[int], l: int, r: int) -> int:
    """取模减法必须加mod防负数"""
    return (S[r] - S[l - 1] + mod) % mod

陷阱(S[r] - S[l-1]) % modS[r] < S[l-1] 时产生负数。因为 S[r]S[l-1] 都是模后的值,真实差可能是 mod + S[r] - S[l-1]


三、前缀和 + 哈希表:子数组问题的黄金组合

3.1 核心问题:和为 K 的子数组个数

LeetCode 560

给定数组 nums 和整数 k,求和等于 k 的连续子数组个数。

暴力 O(n²):枚举所有子数组,求和判断。

优化 O(n)
∑j=lraj=k⇔Sr−Sl−1=k⇔Sl−1=Sr−k\sum_{j=l}^{r} a_j = k \Leftrightarrow S_r - S_{l-1} = k \Leftrightarrow S_{l-1} = S_r - kj=l∑raj=k⇔Sr−Sl−1=k⇔Sl−1=Sr−k

对于每个位置 r,只需统计之前有多少个前缀和等于 S_r - k

python 复制代码
from collections import defaultdict

def subarray_sum(nums: list[int], k: int) -> int:
    """
    和为 K 的子数组个数
    时间:O(n),空间:O(n)
    """
    count = defaultdict(int)
    count[0] = 1  # S_0 = 0 出现1次(空前缀)
    
    prefix = 0
    ans = 0
    
    for num in nums:
        prefix += num
        ans += count[prefix - k]  # 之前有多少个 S = prefix - k
        count[prefix] += 1         # 当前前缀和入表
    
    return ans


# 验证
print(subarray_sum([1, 1, 1], 2))      # 2: [1,1], [1,1]
print(subarray_sum([1, 2, 3], 3))      # 2: [1,2], [3]
print(subarray_sum([1, -1, 0], 0))     # 3: [1,-1], [0], [1,-1,0]

执行过程可视化nums = [1,1,1], k = 2):

步骤 num prefix 需要 count[prefix-2] count 状态 ans
初始 --- 0 --- {0:1} 0
i=0 1 1 count[-1]=0 {0:1, 1:1} 0
i=1 1 2 count[0]=1 ✅ {0:1, 1:1, 2:1} 1
i=2 1 3 count[1]=1 ✅ {0:1, 1:1, 2:1, 3:1} 2

3.2 变体:最长平衡子串

问题 :字符串中只有 LQ,求最长的平衡子串(LQ 数量相等)。

转化L → +1Q → -1,问题变为"和为 0 的最长子数组"。

python 复制代码
def longest_balanced(s: str) -> int:
    """
    最长平衡子串:L和Q数量相等
    时间:O(n),空间:O(n)
    """
    diff = [1 if c == 'L' else -1 for c in s]
    
    prefix = 0
    first = {0: -1}  # 前缀和值 → 第一次出现的下标
    ans = 0
    
    for i, d in enumerate(diff):
        prefix += d
        if prefix in first:
            ans = max(ans, i - first[prefix])
        else:
            first[prefix] = i  # 只记录第一次出现,保证最长
    
    return ans


# 验证
print(longest_balanced("LLQQ"))       # 4: 整个字符串
print(longest_balanced("LQLLQQ"))     # 4: "LLQQ" (下标1-4)
print(longest_balanced("LLL"))        # 0: 无法平衡

为什么记录第一次出现? 因为 i - first[prefix] 是最大跨度。后面再遇到相同 prefixi 更大,差值也更大,但第一次出现的位置决定了最长距离。


四、二维前缀和:矩阵区间查询

4.1 构建原理(容斥原理)

复制代码
        y1    y2
    ┌─────┬─────┐
 x1 │  A  │  B  │
    ├─────┼─────┤
 x2 │  C  │  D  │  ← 目标区域
    └─────┴─────┘

S[x2][y2] = A+B+C+D
S[x1-1][y2] = A+C
S[x2][y1-1] = A+B
S[x1-1][y1-1] = A

D = S[x2][y2] - S[x1-1][y2] - S[x2][y1-1] + S[x1-1][y1-1]

4.2 完整代码

python 复制代码
def build_2d_prefix(matrix: list[list[int]]) -> list[list[int]]:
    """
    构建二维前缀和
    时间:O(n·m),空间:O(n·m)
    """
    if not matrix or not matrix[0]:
        return [[]]
    
    n, m = len(matrix), len(matrix[0])
    S = [[0] * (m + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            S[i][j] = (S[i-1][j] + S[i][j-1] 
                      - S[i-1][j-1] + matrix[i-1][j-1])
    
    return S

def query_2d(S: list[list[int]], 
           x1: int, y1: int, x2: int, y2: int) -> int:
    """
    查询子矩阵和,坐标为1-indexed闭区间
    """
    return (S[x2][y2] - S[x1-1][y2] 
            - S[x2][y1-1] + S[x1-1][y1-1])


# 验证
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]
S = build_2d_prefix(matrix)
print(query_2d(S, 1, 1, 2, 2))  # 1+2+4+5 = 12
print(query_2d(S, 2, 2, 3, 3))  # 5+6+8+9 = 28

五、差分数组:前缀和的逆运算

5.1 核心关系

操作 正向 逆向
数组 → 前缀和 积分(累加) ---
差分 → 数组 --- 微分(相邻差)

关键性质:对差分数组求前缀和 = 原数组;对原数组求差分 = 差分数组。

5.2 区间修改的 O(1) 优化

场景 :对数组的某个区间 [l, r] 全部加 v,最后统一查询。

暴力:每次修改 O(r-l+1),m 次修改 O(m·n)。

差分优化

  • d[l] += v(从 l 开始增加)
  • d[r+1] -= v(从 r+1 恢复原值)
  • 最后对 d 求前缀和还原
python 复制代码
def range_add_diff(n: int, operations: list[tuple[int, int, int]]) -> list[int]:
    """
    多次区间加,最后查询每个位置的值
    operations: [(l, r, v), ...]
    """
    d = [0] * (n + 2)  # 多开2位,避免r+1越界
    
    for l, r, v in operations:
        d[l] += v
        d[r + 1] -= v
    
    # 前缀和还原
    a = [0] * (n + 1)
    a[1] = d[1]
    for i in range(2, n + 1):
        a[i] = a[i - 1] + d[i]
    
    return a[1:]


# 验证
ops = [(1, 3, 2), (2, 4, 3), (1, 5, 1)]
# 位置1: 2+1=3, 位置2: 2+3+1=6, 位置3: 2+3+1=6, 位置4: 3+1=4, 位置5: 1
print(range_add_diff(5, ops))  # [3, 6, 6, 4, 1]

适用条件:多次区间修改,少量(或最后统一)查询。与前缀和恰好互补。


六、进阶:前缀和与双指针

6.1 和 ≤ K 的子数组个数(元素非负)

当数组元素全部非负时,子数组和具有单调性:右端点右移,和增大;左端点右移,和减小。

python 复制代码
def count_subarrays_leq_k(nums: list[int], k: int) -> int:
    """
    统计和 <= K 的子数组个数
    前提:nums 中所有元素 >= 0
    时间:O(n)
    """
    n = len(nums)
    S = build_prefix(nums)  # S[i] 表示前i个元素的和
    
    left = 0
    ans = 0
    
    for right in range(1, n + 1):
        # 收缩左边界,直到区间和 <= k
        while left < right and S[right] - S[left] > k:
            left += 1
        # 所有以 right 结尾,left..right-1 开始的子数组都满足
        ans += right - left
    
    return ans


# 验证
print(count_subarrays_leq_k([1, 2, 3, 4], 5))  # 6
# [1], [2], [3], [1,2], [2,3], [1,2,3] (和<=5)

前提破坏 :如果有负数,单调性不成立,需要用有序集合树状数组维护前缀和。


七、常见陷阱与 Checklist

7.1 错误清单

陷阱 后果 修复
下标混乱(0/1混用) WA 统一 1-indexed
取模减法未 + mod 负数 (a - b + mod) % mod
差分 r+1 越界 RE 数组开 n+2
二维查询坐标顺序 算错区域 画图验证 x1≤x2, y1≤y2
哈希表未初始化 count[0]=1 漏解 空前缀 S_0=0 必须计入

7.2 快速决策树

复制代码
问题是否涉及"区间和"或"子数组和"?
├── 是 → 数组是否静态(无修改)?
│       ├── 是 → 查询次数多?前缀和 O(1) 查询
│       └── 否 → 差分数组 O(1) 修改,最后还原
│
└── 是 → 是否求"和为 K 的子数组"?
        ├── 是 → 前缀和 + 哈希表,O(n)
        └── 否 → 元素非负?双指针 O(n)
                └── 有负数?树状数组/线段树 O(n log n)

八、与其他数据结构的对比

结构 预处理 单点修改 区间查询 最佳场景
前缀和 O(n) ❌ 不支持 O(1) 静态、查询极多
差分数组 O(n) O(1)(区间) O(n)(还原) 修改多、最后查
树状数组 O(n) O(log n) O(log n) 动态、两者均衡
线段树 O(n) O(log n) O(log n) 动态、功能复杂

九、核心心法

前缀和的精髓不在于公式,而在于三种思维习惯:

1. 预处理意识

看到"多次查询"立即想:能否 O(n) 预处理,O(1) 回答?

2. 转化能力

将"区间和"转化为"单点差值",将"子数组问题"转化为"两前缀和配对"。

3. 边界把控

下标从 1 开始,S[0]=0,取模减法 + mod。细节决定 AC。

判断标准:当你看到"子数组和为 K"时,能在 10 秒内想到哈希表;看到"矩阵区间求和"时,能瞬间画出容斥图------前缀和才真正成为你的本能。

相关推荐
汉克老师4 小时前
GESP2025年3月认证C++五级( 第二部分判断题(1-10))
c++·算法·分治算法·线性筛法·gesp5级·gesp五级
盼小辉丶4 小时前
PyTorch强化学习实战(4)——PyTorch基础
人工智能·pytorch·python·强化学习
洛水水4 小时前
【力扣100题】17.K 个一组翻转链表
算法·leetcode·链表
YJlio4 小时前
10.2.8 以其他账户运行服务(Running services in alternate accounts):为什么“把服务切到某个用户账号下运行”,本质上是在改变服务的整个安全上下文?
python·安全·ios·机器人·django·iphone·7-zip
运气好好的4 小时前
CSS如何实现响应式内边距自适应_利用vw单位动态调整
jvm·数据库·python
技术钱5 小时前
LangChain简介
python·langchain
洛水水5 小时前
【力扣100题】16.两两交换链表中的节点
算法·leetcode·链表
4***17545 小时前
3.3 Python图形编程
开发语言·python·pygame
wuweijianlove5 小时前
算法教学中的抽象建模与动态可视化设计的技术7
算法