跳转到内容

和为 K 的子数组:查询历史前缀

约 4 分钟原样导入

← 前缀和专题目录

02 · 枚举右端点,查询左侧前缀

不再给定一个区间,而是问“有多少段的和恰好等于 K”。同一个前缀值出现多次,意味着有多个可选左边界。

手动推演 · 哨兵与顺序实验 · Java 代码 · 自测

默认数组 [1,2,1,2],K=3。统计和恰好为 K 的非空连续子数组数量,不是任意挑选元素。三个合法闭区间是 [0,1]、[1,2]、[2,3],答案 3。区间可以重叠,位置不同分别计数。

“子数组”必须连续;原数组负数与 0 也合法。前缀 P[t] 仍表示前 t 项的和,原数组区间 [l,r] 的和为 P[r+1]−P[l]。

固定左端点 l,从 r=l 开始向右延伸。每多一格就把 nums[r] 加到 sum,检查一次是否等于 K。换左端点时 sum 清零。

长度 4 的数组共有 4+3+2+1=10 个非空候选区间。这里用增量累加做到 O(n²),不是每次重新求整段和的 O(n³) 写法。下方暴力演示也会显示所有不满足条件的候选。

固定右端点 r,记 cur=P[r+1]。条件 P[r+1]−P[l]=K,移项得到 P[l]=cur−K。于是每来一个右端点,只需问:以前有几个前缀值等于 cur−K?

例如默认样例处理 i=3 时,cur=6,需要历史前缀 3;P[2]=3,对应左边界 l=2,因此新增区间 [2,3]。历史表的键是前缀和,不是原数组元素值。

freq 保存次数。多个相同值的历史前缀对应不同左边界,都应该计数,不能改用只保存存在与否的 Set。

开始前放入 freq[0]=1,表示 P[0]=0 出现过一次。当 cur=K 时,目标前缀为 0,这样才能计入从原数组下标 0 开始的区间。

这不是预先把空子数组算作答案。查询时,只允许更早的前缀 P[0…r];当前的 P[r+1] 尚未入表。因此区间至少包含一个元素。查询、累加完成后再记录当前前缀。

手动推演:累计、变形、查询、贡献、记录

Section titled “手动推演:累计、变形、查询、贡献、记录”

每轮五个动作。查询命中后才把贡献加入 ans;记录当前前缀时,图中“历史覆盖范围”会随之更新。

演示尚未初始化;如果此提示持续显示,脚本未能加载。文字与 Java 示例仍可阅读。

交互推演
前缀和实验室 · 亲手推演初始状态

已应用数据 等待初始化正确做法

原数组 nums · 格子上的数字是元素,下标从 0 开始

前缀数组 P / pre · 格子下标是取了多少个元素

P[t] 表示 [0,t) 的和。? 表示尚未算出,P[0]=0 是空前缀。

初始状态

点击下一步开始

累计数量 ans尚未计算
当前前缀 cur0
本轮命中 hit未查询
状态详情 · 边界与查询依据

前缀位置与区间列表仅供教学;Java 频次表存次数,不存全部位置。表中键按数值升序排版,不代表 HashMap 的遍历顺序。

复杂度账本

记录本次推演的算法动作,不代表 JVM 指令或播放性能。

支持 1~12 个 -99~99 的整数、中英文逗号、一对方括号。K:-999~999。允许负数与零。限制用于保证画面可读。

查看 Java 代码 · 当前动作语句高亮

高亮表示刚执行完成的语句,一步可以覆盖多行;最后一步也包含返回值。代码仅为正确解法,错误实验独立展示。

前缀解法

import java.util.HashMap;import java.util.Map;class Solution {    public long countSubarraysSumK(int[] nums, int k) {        Map<Long, Integer> freq = new HashMap<>();        freq.put(0L, 1);        long cur = 0L, ans = 0L;        for (int x : nums) {            cur += x;            long need = cur - k;            int hit = freq.getOrDefault(need, 0);            ans += hit;            freq.put(cur, freq.getOrDefault(cur, 0) + 1);        }        return ans;    }}

暴力解法

class Solution {    public long countSubarraysSumK(int[] nums, int k) {        long ans = 0L;        for (int left = 0; left < nums.length; left++) {            long sum = 0L;            for (int right = left; right < nums.length; right++) {                sum += nums[right];                if (sum == k) ans++;            }        }        return ans;    }}

cur 与 ans 都用 long,避免前缀总和或子数组数量超出 int。增强 for 中没有数组下标变量,图中的 i 是教学定位信息。

查询的键是 cur - k,更新的键是 cur。它们通常不是同一个键,不能用 hit + 1 更新当前前缀次数。

本页是与 Java 示例对应的教学推演,不运行任意 Java。加载完成后可断网操作;刷新清空输入、步骤和自测答案。

默认样例逐轮核对(记录当前前缀之后)
原数组 i cur=P[i+1] need=cur−3 hit ans
0 1 −2 0 0
1 3 0 1 1
2 4 1 1 2
3 6 3 1 3

每个合法区间有唯一的右端点,处理这个右端点时,它的左前缀已经在历史中,且满足 cur−K。频次精确表示可选左边界数量,因此不漏、不重复。

暴力检查 n(n+1)/2 个候选,时间 O(n²)、额外空间 O(1)。优化每个元素做一次前缀累加、一次匹配查询、一次频次记录,HashMap 平均查改 O(1),总体平均 O(n)、额外空间 O(n)。更新语句内部读取已有次数,账本中的“匹配查询”仅指寻找 need 的那次查询。

图中为了教学保留的全部区间和快照,不是 Java 模板实际保存的数据。含负数时窗口扩大会使和变小,不能无条件使用滑动窗口替代。

两个错误实验:独立输入 [0],K=0

Section titled “两个错误实验:独立输入 [0],K=0”

正确做法得到 1,只有区间 [0,0]。漏掉空前缀会得到 0;提前记录当前前缀会得到 2,多算一个空区间 [1,0],也就是左前缀与当前前缀是同一个位置。

演示尚未初始化;如果此提示持续显示,脚本未能加载。文字与 Java 示例仍可阅读。

独立实验
独立错误实验 · 固定输入初始状态

已应用数据 等待初始化正确做法

原数组 nums · 格子上的数字是元素,下标从 0 开始

前缀数组 P / pre · 格子下标是取了多少个元素

P[t] 表示 [0,t) 的和。? 表示尚未算出,P[0]=0 是空前缀。

初始状态

点击下一步开始

累计数量 ans尚未计算
当前前缀 cur0
本轮命中 hit未查询
状态详情 · 边界与查询依据

前缀位置与区间列表仅供教学;Java 频次表存次数,不存全部位置。表中键按数值升序排版,不代表 HashMap 的遍历顺序。

复杂度账本

记录本次推演的算法动作,不代表 JVM 指令或播放性能。

顺序反转造成自匹配的直接反例是 K=0;不能声称任何非零 K 都必然多算一段。错误实验独立于主样例,进入时暂停主演示。

Java:查询键与更新键不是一回事

Section titled “Java:查询键与更新键不是一回事”

查看联动 Java。查询的是 need=cur−K,记录的是 cur,不能拿 hit+1 更新 freq[cur]。模板从原稿的一行查询累加中拆出 need 与 hit,便于对照步骤。

cur 与 ans 用 long:总和可能超出 int,10 万个零、K=0 时有 5,000,050,000 个非空区间。图中的原数组下标 i 是教学定位,增强 for 实际只声明 x。

自测尚未初始化;可先用纸笔作答。

独立自测样例:[1, -1, 1],K=0

3. 以下哪项说法正确?

依据 S2 技巧 2 的默认例子和 560 模板、S1 哨兵 A/B 与单步账本训练、S3 §5 的错误清单整理。完整来源与校对说明。

下一课:整除子数组 →