Pattern 13 · Prefix Sum

13 前缀和 一次预处理,之后区间和 O(1);配哈希解「子数组和为 K」

面试频率 高题目 4 道模板 2 个⭐认脸 0 道
§1 Mental Model 一句话心智模型

先记住这一句

前缀和把「区间和」变成两个数的差:sum(i..j) = pre[j+1] - pre[i]。一旦区间和能 O(1) 拿到,问题就退化成「找一对下标」——那是第 01 章的活。

这两步是本章的全部:先把「区间」降维成「一对前缀」,再用哈希表回答「我要的那个前缀值,之前出现过几次 / 最早在哪」。「子数组和恰好为 K」于是变成:对每个右端点,问 cur - k 见过几次。

累积量不必是和。取模后能处理「能被 K 整除」,异或前缀能处理「异或为 0 的子数组」,前后缀乘积能处理 238。骨架不动,换的只是「累积什么」和「找哪个历史值」。

§2 Recognition Signals 识别信号

看到什么,想到本章

题面出现…想到
「区间和 / 范围求和」且要多次查询预处理前缀和数组(模板 A)
「和恰好为 K 的子数组个数 / 最长长度」前缀和 + 哈希(模板 B)
「除自身以外数组的乘积」且不许用除法前缀积 × 后缀积
「能被 K 整除的子数组」前缀和取模,同余即可整除
「0 和 1 数量相同的最长子数组」把 0 记成 −1,转成「和为 0」
二维矩阵的「子矩形和」二维前缀和,容斥四项
分流条件 · 决策树同源

都带「连续子数组」四个字,分流看的是问什么:「和恰好为 K」→ 本章(元素可能为负时窗口和不单调,滑窗的收缩条件无从写起;全为正数时滑窗也做得了,但前缀和一招通吃);「和 ≥ target 的最短」且元素全为正 → 滑动窗口(第 03 章);「窗口固定大小求最值」→ 单调队列(第 04 章)。本章的哈希只是配件,主语始终是前缀和——这一点决定了你该往哪本模板里翻。

§3 Template 模板代码

可默写的那几行

■ 【骨架】 不变,闭着眼默写 ■ 【插槽】 随题变化,写题前先问自己填什么

模板 A · 前缀和数组多次区间查询
def build_prefix(nums):
    pre = [0] * (len(nums) + 1)             # 【骨架】长度 n+1,pre[0]=0 是哨兵
    for i, x in enumerate(nums):            # 【骨架】
        pre[i + 1] = pre[i] + x             # 【插槽】累积什么:和 / 乘积 / 异或 / 计数
    return pre

def range_sum(pre, i, j):                   # 【骨架】闭区间 [i, j]
    return pre[j + 1] - pre[i]              # 【骨架】区间 = 两个前缀之差
默写口诀:长度 n+1、pre[0]=0、区间取两端之差。哨兵位省掉了所有 i == 0 的特判。
模板 B · 前缀和 + 哈希「恰好为 K」类
from collections import defaultdict

def subarray_sum_k(nums, k):
    cnt = defaultdict(int)
    cnt[0] = 1                              # 【骨架】空前缀出现过一次;漏了它就漏掉"从头开始"的子数组
    cur = ans = 0
    for x in nums:                          # 【骨架】
        cur += x                            # 【插槽】累积量:和 / 前缀和取模 / 奇偶数差
        ans += cnt[cur - k]                 # 【插槽】要找的历史值,以及答案怎么攒:计数 / 最长 / 存在性
        cnt[cur] += 1                       # 【骨架】先结算再登记,否则会用自己配自己
    return ans
两条铁律:cnt[0] = 1 必须有(代表空前缀),先结算再登记(否则会拿自己配自己,和第 01 章模板 A 是同一条纪律)。
§4 Problems 题目分层

从建直觉到扭模板

热身建直觉,easy 为主1 道
  • 模板 A 原题。它存在的意义就是让你记住「预处理一次,之后每次查询 O(1)」这个交易——以及 n + 1 长度的哨兵位。
标准模板直接套2 道
  • 前缀积 + 后缀积。第二遍用一个滚动变量 right 就能做到 O(1) 额外空间(输出数组不算)。被追问「有 0 怎么办」时,正是不用除法的理由。
  • 模板 B 原题,本章最该背下来的一道。面试高频追问:「全是正数能不能用滑窗」——能,但有负数就只能靠它。
变体模板要扭,坑的来源1 道
  • 累积量换成 前缀和 % k,哈希里存最早下标而不是次数。两个坑:长度至少为 2,以及 first = {0: -1} 这个初始值。
§5 Pitfalls & Self-check 常见翻车点 + 自测

会写不等于不翻车

  1. 忘了 cnt[0] = 1——少了它,「从下标 0 开始的那些子数组」全部数不到。subarray_sum_k([3], 3) 会返回 0——模板测试里专门留了这条断言。
  2. 先登记再结算——cnt[cur] += 1 必须在 ans += cnt[cur - k] 之后。顺序反了,k == 0 时每个位置都会多算一次。
  3. 下标偏移写错——pre 长度是 n + 1,闭区间 [i, j] 的和是 pre[j+1] - pre[i]。把 pre 写成长度 n(pre[i] 表示前 i+1 项)也能做,但每道题都要重新推边界,得不偿失。
  4. 对负数用滑窗——「和为 K 的子数组」在有负数时不能滑窗:扩大窗口不再让和单调变大,收缩条件无从写起。这正是本章存在的理由。
  5. 523 只判同余不判长度——题目要求子数组长度至少为 2,所以哈希里要存最早下标并检查 i - first[cur] >= 2。存最新下标会把长度 1 的情况误判成 True。

自测 · 30 秒识别模式

30 秒识别Q1 · 724 Find Pivot Index:找出数组中的「中心下标」:它左边所有数之和等于右边所有数之和。你的第一反应是哪个模式?

看答案

前缀和。left == total - left - nums[i] 一次遍历即可。看到「左边的和 / 右边的和」就是前缀和的字面形态,连哈希都不需要。

30 秒识别Q2 · 974 Subarray Sums Divisible by K:求和能被 K 整除的连续子数组个数。你的第一反应是哪个模式?

看答案

前缀和 + 哈希(模板 B),累积量换成 前缀和 % k:两个前缀同余,它们之间那段就能被 K 整除。注意 Python 的 % 对负数已经返回非负结果,其他语言要自己修正。

30 秒识别Q3 · 求长度恰好为 k 的连续子数组的最大平均值。你的第一反应是哪个模式?

看答案

定长滑动窗口,不是本章——窗口大小固定,进一个出一个就能 O(1) 维护和,根本不需要建前缀数组。前缀和是为任意区间的多次查询准备的;只查一种固定长度时它是杀鸡用牛刀。见 §2 分流条件与第 03 章模板 B。

每章自测都会混入恰好一道非本章题,防止「读哪章选哪章」的条件反射;题面也一律取自本章未收录的题,避免凭记忆而非凭信号作答。