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

    &nbsp;

    **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

    &nbsp;

    **Step 3: Return the result**

    - Return `count` after processing all elements

    &nbsp;

    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.
