和相同的二元子数组:恰好等于 goal
0/1 数组没有改写前缀和。先查「差为 goal 的旧前缀」,或用至多窗口相减。
子数组 [i, j) 的和 = pre[j] − pre[i]。要它等于 goal,就是数有多少个旧前缀等于 pre − goal。哈希表先查后记,与 LC560 同一句话。数组只含 0/1 时,窗口和随左端收缩单调下降,于是可以算 g(x)=「和 ≤ x 的子数组个数」,答案 = g(goal) − g(goal−1)。两种都是 O(n)。
这是 LeetCode 930. Binary Subarrays With Sum。给你只含 0 和 1 的数组 nums,再给一个整数 goal,统计有多少个连续子数组的和恰好等于 goal。子数组不能挖空,空段不算。
主例 nums = [1, 0, 1, 0, 1],goal = 2。和为 2 的四段是 [1,0,1](下标 0..2)、[1,0,1,0](0..3)、[0,1,0,1](1..4)、[1,0,1](2..4)。答案是 4。goal = 0 时,每一段连续的 0 都要按长度计数,不能漏。
枚举左右端点再求和是平方级。缺的不是「哪一段加起来是 2」,而是走到右端点 j 时,左边有多少个位置已经把前缀和堆到 pre[j] − goal。0/1 另开一扇门:窗口和不会突然跳,恰好等于可以用两个「至多」相减。本文先用手走完前缀和,再把 4 拆成 g(2) − g(1)。
前缀和差为 goal,先查后记
定义 pre 为从开头累加到当前的和。任意一段 [i, j] 的和等于「到 j 的前缀」减「到 i−1 的前缀」。要这段和等于 goal,就是问:在当前 pre 之前,有多少个前缀恰好等于 pre − goal。
哈希表 freq 记每个前缀和出现了几次,一开始 freq[0] = 1,表示空前缀。每读一个数:先 pre += x,再 ans += freq[pre − goal],最后 freq[pre]++。先查后记,保证左端点严格在当前格子左边。0/1 只让 pre 每次加 0 或加 1,公式和 LC560 没有区别。
用手走主例,goal = 2。读 1:pre=1,查 freq[−1] = 0,登记 1。读 0:pre 仍是 1,再查不到 −1,freq[1] 变成 2。读 1:pre=2,查 freq[0] = 1,命中从开头到这里的 [1,0,1],ans=1。读 0:pre 仍是 2,再查 freq[0] = 1,命中 [1,0,1,0],ans=2。
读最后一个 1:pre=3,查 freq[1] = 2。此前前缀和为 1 的位置有两个——读完第一项之后、以及读完前两项之后——分别对应 [0,1,0,1] 和 [1,0,1]。这一步加上 2,ans=4。演示停在 pre=3、查到 freq[1]=2 这一帧。四个命中全部来自「旧前缀 = 当前前缀 − 2」,没有漏、没有重。
恰好 = 至多 goal − 至多 goal−1
前缀和哈希对任意整数都成立。本题数组只含 0/1,窗口和随左指针右移只会减 0 或减 1,不会跳着减。于是「和至多是 x」可以用滑动窗口在线性时间数完:右端扩张,窗口和超过 x 就收缩左端;每固定一个右端,左端到右端之间每一个起点都合法,贡献 right−left+1。
设 g(x) 为和 ≤ x 的子数组个数。和恰好为 goal 的个数就是 g(goal) − g(goal−1)。goal = 0 时 g(−1) 定义为 0,因为和不能为负。
主例手算 g(2)。窗口从左往右:[1]、[1,0]、[1,0,1]、[1,0,1,0] 都 ≤ 2;加上最后一个 1 后和变成 3,丢掉最左的 1,窗口变成 [0,1,0,1],和回到 2。五个右端点分别贡献 1、2、3、4、4,g(2)=14。g(1) 同样滑:右端走到第三个 1 时和升到 2,左端要收到只剩 [0,1],五个右端点贡献 1、2、2、3、2,g(1)=10。14 − 10 = 4,与哈希法同一答案。
为什么 0/1 才能这样减窗口和必须随左端收缩单调下降,g(x) 才能用双指针。数组里若有 2 或 −1,收缩一步可能跳过 goal,至多相减会漏。那时只剩前缀和哈希。
Go:前缀和哈希
func numSubarraysWithSum(nums []int, goal int) int {freq := map[int]int{0: 1}pre, ans := 0, 0for _, x := range nums {pre += xans += freq[pre-goal]freq[pre]++}return ans}
1freq[0]=1 把「从开头到当前」这一段算进去。主例第一次 pre=2 时,正是靠它命中 [1,0,1]。
2pre 只加 0 或 1,但查的是 pre−goal,和数组是不是二元无关。
3先加 freq[pre−goal],再 freq[pre]++。反过来会把当前格子当成自己的左端点。
4主例最后一步 pre=3,freq[1] 仍是 2,一次加上两段,答案到 4。
总结
pre 差为 goal 就数一段。主例最后 pre=3 查到两个 1,四种切法凑齐 4。
- 0/1 是障眼法。前缀和哈希与 LC560 同一句:先查 pre−goal,再登记 pre。
- 恰好可以用两个至多相减,前提是窗口和单调。主例 g(2)−g(1)=14−10=4。
- goal=0 时不要特判丢空;哈希的 freq[0] 和 g(−1)=0 各自能处理。