226 lines
9.7 KiB
YAML
226 lines
9.7 KiB
YAML
title: Binary Subarrays With Sum
|
|
slug: binary-subarrays-with-sum
|
|
difficulty: medium
|
|
leetcode_id: 930
|
|
leetcode_url: https://leetcode.com/problems/binary-subarrays-with-sum/
|
|
categories:
|
|
- arrays
|
|
- hash-tables
|
|
patterns:
|
|
- slug: sliding-window
|
|
is_optimal: true
|
|
- slug: prefix-sum
|
|
is_optimal: false
|
|
|
|
function_signature: "def num_subarrays_with_sum(nums: list[int], goal: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [1, 0, 1, 0, 1], goal: 2 }
|
|
expected: 4
|
|
- input: { nums: [0, 0, 0, 0, 0], goal: 0 }
|
|
expected: 15
|
|
hidden:
|
|
- input: { nums: [1, 1, 1, 1, 1], goal: 3 }
|
|
expected: 3
|
|
- input: { nums: [1, 0, 0, 0, 1], goal: 1 }
|
|
expected: 8
|
|
- input: { nums: [0, 1, 0], goal: 1 }
|
|
expected: 4
|
|
- input: { nums: [1], goal: 1 }
|
|
expected: 1
|
|
- input: { nums: [0], goal: 0 }
|
|
expected: 1
|
|
- input: { nums: [1, 1, 1], goal: 5 }
|
|
expected: 0
|
|
|
|
description: |
|
|
Given a binary array `nums` and an integer `goal`, return *the number of non-empty **subarrays** with a sum equal to* `goal`.
|
|
|
|
A **subarray** is a contiguous part of the array.
|
|
|
|
constraints: |
|
|
- `1 <= nums.length <= 3 * 10^4`
|
|
- `nums[i]` is either `0` or `1`
|
|
- `0 <= goal <= nums.length`
|
|
|
|
examples:
|
|
- input: "nums = [1,0,1,0,1], goal = 2"
|
|
output: "4"
|
|
explanation: "The 4 subarrays with sum 2 are: [1,0,1], [1,0,1,0], [0,1,0,1], and [1,0,1]."
|
|
- input: "nums = [0,0,0,0,0], goal = 0"
|
|
output: "15"
|
|
explanation: "Every non-empty subarray has sum 0. With 5 elements, there are 5 + 4 + 3 + 2 + 1 = 15 subarrays."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're walking along the array, keeping a running total of all the `1`s you've seen so far. At any position, your running total is called the **prefix sum**.
|
|
|
|
Here's the key insight: if you know the prefix sum at position `j` is `P[j]`, and you want to find subarrays ending at `j` with sum equal to `goal`, you need to find earlier positions `i` where `P[i] = P[j] - goal`. Why? Because `P[j] - P[i]` gives you the sum of elements between positions `i+1` and `j`.
|
|
|
|
Think of it like this: you're standing at a point on a trail and you've climbed 10 metres total from the start. If you want to find segments of trail where you climbed exactly 3 metres, you need to find all earlier points where you had climbed exactly 7 metres (10 - 3 = 7). The number of such points equals the number of valid segments ending at your current position.
|
|
|
|
By using a hash map to count how many times each prefix sum has occurred, we can instantly look up how many valid subarrays end at any position.
|
|
|
|
approach: |
|
|
We solve this using a **Prefix Sum with Hash Map** approach:
|
|
|
|
**Step 1: Initialise tracking variables**
|
|
|
|
- `count`: Set to `0` to accumulate the total number of valid subarrays
|
|
- `prefix_sum`: Set to `0` to track the running sum as we iterate
|
|
- `prefix_count`: A hash map initialised with `{0: 1}` — this handles subarrays starting from index 0
|
|
|
|
|
|
|
|
**Step 2: Iterate through the array**
|
|
|
|
- For each element, add it to `prefix_sum`
|
|
- Calculate `target = prefix_sum - goal`
|
|
- If `target` exists in `prefix_count`, add `prefix_count[target]` to `count` — this is the number of subarrays ending at the current index with sum equal to `goal`
|
|
- Increment `prefix_count[prefix_sum]` by 1 to record this prefix sum for future iterations
|
|
|
|
|
|
|
|
**Step 3: Return the result**
|
|
|
|
- Return `count` after processing all elements
|
|
|
|
|
|
|
|
The hash map approach transforms what would be an O(n^2) problem (checking all subarrays) into an O(n) solution by leveraging the prefix sum property.
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting the Initial Prefix Sum
|
|
description: |
|
|
A common mistake is not initialising the hash map with `{0: 1}`.
|
|
|
|
Consider `nums = [1, 1]` with `goal = 2`. The prefix sums are `[1, 2]`. When we reach prefix sum 2 and look for `2 - 2 = 0`, we need to find one occurrence of 0 to count the subarray `[1, 1]`.
|
|
|
|
Without the initial `{0: 1}`, subarrays that start from index 0 and sum to `goal` would be missed entirely.
|
|
wrong_approach: "Initialise prefix_count as empty {}"
|
|
correct_approach: "Initialise prefix_count with {0: 1}"
|
|
|
|
- title: The Brute Force Trap
|
|
description: |
|
|
The naive approach checks every possible subarray using nested loops:
|
|
- Outer loop for start index `i`
|
|
- Inner loop for end index `j`
|
|
- Calculate sum for each `(i, j)` pair
|
|
|
|
This results in **O(n^2) or O(n^3) time complexity**. With the constraint `nums.length <= 3 * 10^4`, this means up to ~1 billion operations, causing **Time Limit Exceeded (TLE)**.
|
|
|
|
The prefix sum approach reduces this to O(n) by avoiding redundant sum calculations.
|
|
wrong_approach: "Nested loops checking all subarrays"
|
|
correct_approach: "Prefix sum with hash map for O(n) lookup"
|
|
|
|
- title: Handling goal = 0 Incorrectly
|
|
description: |
|
|
When `goal = 0`, we're looking for subarrays of consecutive zeros. This is where the algorithm handles an edge case elegantly.
|
|
|
|
For `nums = [0, 0]`, the prefix sums are `[0, 0]`. At each zero, we look for `prefix_sum - 0 = prefix_sum` in our map. The count naturally accumulates because each repeated prefix sum value indicates another valid subarray.
|
|
|
|
Some implementations fail here by not properly counting the growing number of ways to form zero-sum subarrays.
|
|
|
|
key_takeaways:
|
|
- "**Prefix sum transformation**: Converting subarray sum problems into prefix difference problems is a powerful technique"
|
|
- "**Hash map for O(1) lookup**: Storing prefix sum frequencies enables instant counting of valid subarrays"
|
|
- "**Initial state matters**: The `{0: 1}` initialisation handles subarrays starting from index 0"
|
|
- "**Related problems**: This pattern applies to Subarray Sum Equals K, Contiguous Array, and many other subarray counting problems"
|
|
|
|
time_complexity: "O(n). We traverse the array once, performing O(1) hash map operations at each step."
|
|
space_complexity: "O(n). In the worst case, all prefix sums are unique, requiring O(n) space in the hash map."
|
|
|
|
solutions:
|
|
- approach_name: Prefix Sum with Hash Map
|
|
is_optimal: true
|
|
code: |
|
|
def num_subarrays_with_sum(nums: list[int], goal: int) -> int:
|
|
count = 0
|
|
prefix_sum = 0
|
|
# {0: 1} handles subarrays starting from index 0
|
|
prefix_count = {0: 1}
|
|
|
|
for num in nums:
|
|
# Update running prefix sum
|
|
prefix_sum += num
|
|
|
|
# How many earlier positions have prefix_sum - goal?
|
|
# Each such position marks the start of a valid subarray
|
|
target = prefix_sum - goal
|
|
if target in prefix_count:
|
|
count += prefix_count[target]
|
|
|
|
# Record this prefix sum for future iterations
|
|
prefix_count[prefix_sum] = prefix_count.get(prefix_sum, 0) + 1
|
|
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single pass through the array with O(1) hash map operations.
|
|
|
|
**Space Complexity:** O(n) — Hash map stores at most n+1 unique prefix sums.
|
|
|
|
This approach uses the prefix sum property: `sum(i, j) = prefix[j] - prefix[i-1]`. By maintaining a count of each prefix sum seen so far, we can instantly find how many subarrays ending at the current position have the target sum.
|
|
|
|
- approach_name: Sliding Window
|
|
is_optimal: true
|
|
code: |
|
|
def num_subarrays_with_sum(nums: list[int], goal: int) -> int:
|
|
def at_most(k: int) -> int:
|
|
"""Count subarrays with sum <= k."""
|
|
if k < 0:
|
|
return 0
|
|
|
|
count = 0
|
|
left = 0
|
|
current_sum = 0
|
|
|
|
for right in range(len(nums)):
|
|
current_sum += nums[right]
|
|
|
|
# Shrink window if sum exceeds k
|
|
while current_sum > k:
|
|
current_sum -= nums[left]
|
|
left += 1
|
|
|
|
# All subarrays ending at right with sum <= k
|
|
count += right - left + 1
|
|
|
|
return count
|
|
|
|
# Subarrays with sum exactly k = at_most(k) - at_most(k-1)
|
|
return at_most(goal) - at_most(goal - 1)
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Two passes, each O(n).
|
|
|
|
**Space Complexity:** O(1) — Only uses a few variables.
|
|
|
|
This approach uses a clever trick: subarrays with sum exactly `k` equals subarrays with sum at most `k` minus subarrays with sum at most `k-1`. The sliding window efficiently counts "at most" subarrays because we can shrink the window when the sum exceeds the threshold.
|
|
|
|
- approach_name: Brute Force
|
|
is_optimal: false
|
|
code: |
|
|
def num_subarrays_with_sum(nums: list[int], goal: int) -> int:
|
|
count = 0
|
|
n = len(nums)
|
|
|
|
# Try every starting position
|
|
for i in range(n):
|
|
current_sum = 0
|
|
# Extend subarray from i to j
|
|
for j in range(i, n):
|
|
current_sum += nums[j]
|
|
if current_sum == goal:
|
|
count += 1
|
|
# Optimisation: early exit if sum exceeds goal (since all nums >= 0)
|
|
elif current_sum > goal:
|
|
break
|
|
|
|
return count
|
|
explanation: |
|
|
**Time Complexity:** O(n^2) — Nested loops checking all subarrays.
|
|
|
|
**Space Complexity:** O(1) — Only tracking current sum and count.
|
|
|
|
This checks every subarray starting at each index. The early break when sum exceeds goal provides some optimisation, but worst case (when goal is large or many zeros) remains O(n^2). Included to illustrate why the prefix sum approach is necessary for large inputs.
|