LeetCode LCR 010. 和为 K 的子数组
题目描述

题意分析
给定整数数组与目标
k,统计和恰好等于k的非空连续子数组个数。相同数值序列出现在不同位置时,也对应不同子数组。元素可以为负或为零,目标也不限定为正。扩张区间可能增加、减少或保持区间和,因此不能直接套用“和大了就缩窗”的正数滑动窗口规则。
解法:前缀和频次配对
核心思路
[!blue]
定义前缀和为从数组开头到某个位置的累计值,另有一个位于第一个元素之前、值为零的空前缀。任意连续区间的和,都等于结束位置的前缀和减去开始位置之前的前缀和。
扫描到当前右端,累计值为
s时,要让区间和等于k,就需要一个更早的前缀和值为s - k。因此不必逐个枚举左端,只要知道这个历史前缀值出现过几次,就能一次加入相同数量的合法区间。哈希表
cnt保存每个历史前缀和的出现次数。同一个值可能来自不同位置,每个位置都给出一个不同的左端,所以必须累加频次,不能只保存是否出现或某一个下标。初始的
cnt[0] = 1代表空前缀,使从数组开头开始的答案也能配对。之后如果再次出现零和前缀,仍正常增加它的频次;空前缀只是其中一个来源,并不是值为零的前缀只能出现于开头。每轮先将当前元素加入
s,再查询cnt[s - k],最后登记当前s。查询时表里只包含更早前缀,保证区间非空;若先登记,k == 0时当前前缀会与自身配对,错误计入空区间。每个区间只在它的右端被统计一次。
解题步骤
- 初始化前缀和与答案为零,预置空前缀的频次
cnt[0] = 1。- 遍历元素并更新当前前缀和
s。- 将历史表中
s - k的次数加入答案,未出现时贡献零。- 再把当前
s的出现次数加一,留给后续右端使用。- 返回累计的子数组数量。
代码实现
class Solution {
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> cnt = new HashMap<>();
// 空前缀:让「从下标 0 开始」的子数组也能被配对。
cnt.put(0, 1);
int answer = 0;
int s = 0;
for (int x : nums) {
s += x;
// 必须先查后写,否则 k == 0 时当前前缀和会和自己配对。
answer += cnt.getOrDefault(s - k, 0);
cnt.merge(s, 1, Integer::sum);
}
return answer;
}
}
func subarraySum(nums []int, k int) (answer int) {
cnt := map[int]int{0: 1}
s := 0
for _, x := range nums {
s += x
answer += cnt[s-k]
cnt[s]++
}
return
}
复杂度分析
设数组长度为 $n$。
- 时间复杂度:期望 $O(n)$,每个元素只触发常数次哈希操作。
- 辅助空间复杂度:$O(n)$,最多记录 $n+1$ 种前缀和值,包含空前缀。
关键点总结
[!green]
- 区间和转换为前缀差,固定右端后查询历史互补值。
- 求数量就存频次,重复前缀对应不同的合法左端。
- 空前缀覆盖从头开始的区间,先查询后登记排除空区间。
易错点总结
[!yellow]
cnt[0]必须初始化为一,空前缀确实存在一次。- 前缀和可能反复相同,覆盖为一次或只使用集合都会少计答案。
- 先登记当前前缀会在
k == 0时产生自身配对。- 查询对象是
s - k,不是k - s,它表示区间左端之前的前缀和。- 不要混用求最长区间时保存最早位置的策略,本题需要所有出现位置的计数。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 523. 连续的子数组和 | 中等 | 同样比较两个前缀,原题把相等差值条件改为前缀余数相等,并限制最小长度。 |
| 437. 路径总和 III | 中等 | 把数组前缀和计数推广到树上向下路径,需要退出分支时撤销前缀频次。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!