A clear explanation of solving Binary Subarrays With Sum using prefix sums and a frequency map.
Problem Restatement
We are given a binary array nums and an integer goal.
A binary array means every value is either:
0or:
1We need to count how many non-empty contiguous subarrays have sum exactly equal to goal.
A subarray must be continuous. We cannot skip elements.
The official statement asks for the number of non-empty subarrays whose sum is goal, where nums is binary. Example cases include [1,0,1,0,1] with goal = 2, which returns 4, and [0,0,0,0,0] with goal = 0, which returns 15.
Input and Output
| Item | Meaning |
|---|---|
| Input | A binary array nums and an integer goal |
| Output | Number of non-empty subarrays with sum goal |
| Subarray | A contiguous part of the array |
| Values | Each nums[i] is 0 or 1 |
Function shape:
class Solution:
def numSubarraysWithSum(self, nums: list[int], goal: int) -> int:
...Examples
Example 1:
nums = [1, 0, 1, 0, 1]
goal = 2The valid subarrays are:
[1, 0, 1]
[1, 0, 1, 0]
[0, 1, 0, 1]
[1, 0, 1]There are 4 valid subarrays.
So the answer is:
4Example 2:
nums = [0, 0, 0, 0, 0]
goal = 0Every non-empty subarray has sum 0.
There are:
5 + 4 + 3 + 2 + 1 = 15valid subarrays.
So the answer is:
15First Thought: Check Every Subarray
The direct way is to try every starting index and every ending index.
For each start:
- Keep a running sum.
- Extend the end index one step at a time.
- Count the subarray when the running sum equals
goal.
Code:
class Solution:
def numSubarraysWithSum(self, nums: list[int], goal: int) -> int:
n = len(nums)
ans = 0
for left in range(n):
total = 0
for right in range(left, n):
total += nums[right]
if total == goal:
ans += 1
return ansThis is correct, but it checks too many subarrays.
There are O(n^2) subarrays, so this is too slow for large input.
Key Insight
We can use prefix sums.
Let:
prefix[i]be the sum of the first i elements.
The sum of a subarray from left to right is:
prefix[right + 1] - prefix[left]We want this to equal goal.
So:
prefix[right + 1] - prefix[left] = goalRearrange:
prefix[left] = prefix[right + 1] - goalThis means that when we are at the current prefix sum, we only need to know how many earlier prefix sums equal:
current_sum - goalA hash map can store how many times each prefix sum has appeared.
Algorithm
Create a map:
count = {0: 1}This means we have seen prefix sum 0 once before reading any element.
Then scan nums.
For each number:
- Add it to
current. - Compute
need = current - goal. - Add
count[need]to the answer. - Add the current prefix sum to the map.
The order matters.
We count earlier prefix sums before inserting the current prefix sum.
Walkthrough
Use:
nums = [1, 0, 1, 0, 1]
goal = 2Start:
count = {0: 1}
current = 0
ans = 0| Index | Value | current | need = current - goal | Add to ans | ans |
|---|---|---|---|---|---|
| 0 | 1 | 1 | -1 | 0 | 0 |
| 1 | 0 | 1 | -1 | 0 | 0 |
| 2 | 1 | 2 | 0 | 1 | 1 |
| 3 | 0 | 2 | 0 | 1 | 2 |
| 4 | 1 | 3 | 1 | 2 | 4 |
The final answer is:
4The last step adds 2 because prefix sum 1 appeared twice earlier. Each earlier prefix sum gives one valid subarray ending at the current index.
Correctness
The algorithm maintains a frequency map of all prefix sums seen before the current position.
At a current index, let the current prefix sum be current.
A subarray ending at this index has sum goal exactly when its starting prefix sum is:
current - goalThe map tells us how many earlier positions have that prefix sum. Each such earlier position defines one distinct subarray ending at the current index with sum goal.
The algorithm adds exactly that number to the answer.
Then it records the current prefix sum so it can be used by future subarrays.
Every valid subarray has one ending index. When the scan reaches that ending index, the subarray’s starting prefix sum is already in the map, so the subarray is counted.
No invalid subarray is counted, because the algorithm only adds prefixes that satisfy:
current - earlier_prefix = goalTherefore, the final answer is exactly the number of non-empty subarrays with sum goal.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We scan nums once |
| Space | O(n) | The map may store up to n + 1 prefix sums |
Implementation
from collections import defaultdict
class Solution:
def numSubarraysWithSum(self, nums: list[int], goal: int) -> int:
count = defaultdict(int)
count[0] = 1
current = 0
ans = 0
for num in nums:
current += num
need = current - goal
ans += count[need]
count[current] += 1
return ansCode Explanation
We use a map from prefix sum to frequency:
count = defaultdict(int)
count[0] = 1The initial 0 handles subarrays that start at index 0.
For example, if the current prefix sum is already equal to goal, then:
current - goal == 0So the initial prefix sum contributes one valid subarray.
We update the running prefix sum:
current += numThen we find the prefix sum needed to form a subarray with sum goal:
need = current - goalEvery earlier occurrence of need gives one valid subarray ending here:
ans += count[need]Finally, we record the current prefix sum:
count[current] += 1Testing
def run_tests():
s = Solution()
assert s.numSubarraysWithSum([1, 0, 1, 0, 1], 2) == 4
assert s.numSubarraysWithSum([0, 0, 0, 0, 0], 0) == 15
assert s.numSubarraysWithSum([1, 1, 1], 2) == 2
assert s.numSubarraysWithSum([1, 0, 0, 1], 1) == 6
assert s.numSubarraysWithSum([0], 0) == 1
assert s.numSubarraysWithSum([1], 0) == 0
print("all tests passed")
run_tests()| Test | Why |
|---|---|
[1,0,1,0,1], goal 2 | Official example |
All zeros, goal 0 | Many zero-sum subarrays |
[1,1,1], goal 2 | Consecutive ones |
[1,0,0,1], goal 1 | Zeros expand valid windows |
[0], goal 0 | Smallest valid zero case |
[1], goal 0 | No valid zero-sum subarray |