Learn/DSA Patterns
DSA PatternsArrays & Two Pointersmedium14 min read

Subarray Counting — Prefix Sum + Hash Map

Counting subarrays with a given sum looks like an O(n²) problem — until you see that two running totals differing by the target define a valid subarray between them. A hash map turns that into a single O(n) pass.

#prefix-sum#hashmap#subarray#counting#subarrays#beginner#interview
Table of contents

Before we start

You already know prefix sums from pattern 1.06 — they turn range-sum queries into O(1). This chapter is about a different superpower of prefix sums: counting subarrays that satisfy a condition, in a single pass. By the end you will be able to:

  • See why two prefix sums that differ by a target enclose a valid subarray between them.
  • Explain out loud why a hash map of prefix-sum frequencies turns counting into O(1) per element.
  • Recognise this pattern in subarray sum, subarray XOR, and divisibility counting problems.

Stop at every Pause & Think box.


Picture this first (no code yet)

A real-life story

A hiker records her altitude every hour in a journal: the running total of all altitude changes from the start. At hour 0, total = 0. After hour 1, total = +300m. After hour 2, total = +500m. And so on.

She wants to know: in how many time windows did she gain exactly 200m?

Here's the key insight: if the total at hour 7 is 900m and the total at hour 3 was 700m, then between hours 3 and 7 she gained exactly 900 − 700 = 200m.

So she only needs to look at her journal and ask: "For each page I'm reading (with running total T), how many earlier pages had running total T − 200?" Each earlier page with that total is one valid window.

She doesn't need to check every pair of pages. She just scans once, keeping a tally of how many times each running total has appeared before.

The hiker's journal is the prefix sum. The "how many earlier totals equal T − target?" question is answered in O(1) by a hash map. Together: one pass, all subarrays counted.


The actual problem

Given an array of integers (possibly negative) and a target sum k, count the number of contiguous subarrays whose elements sum to exactly k.

input : [1, 1, 1], k = 2
output: 3
(subarrays: [1,1] at indices 0-1, [1,1] at indices 1-2, ... wait, n=3 so:
 [1,1] (0..1), [1,1] (1..2) = 2 subarrays of sum 2 ✓ — but output is 3?)

Wait — let me be precise:

input : [1, 2, 3], k = 3
output: 2
([3] at index 2, [1,2] at indices 0-1)

First, the slow way (so you feel the pain)

The obvious approach: for every pair (i, j), compute the sum of arr[i..j] and check if it equals k.

count = 0
for i in range(n):
    total = 0
    for j in range(i, n):
        total += arr[j]
        if total == k:
            count += 1

For n = 10,000: about 50 million iterations. For n = 100,000: about 5 billion. Classic O(n²) pain.

We need O(n). One pass. No nested loops.


The turning point

Let prefix[i] = sum of the first i elements (prefix[0] = 0 by convention).

The sum of subarray from index j to i-1 (inclusive) is:

prefix[i] - prefix[j] = k

Which means:

prefix[j] = prefix[i] - k

So: to count how many subarrays ending at position i have sum k, count how many earlier prefix sums equal prefix[i] - k.

Pause & think

Suppose you are at position i and the running prefix sum is 9. The target is 4. You want to count subarrays ending here with sum 4.

What value of prefix[j] would you need to have seen at an earlier position j? Why?

You need prefix[j] = prefix[i] - k = 9 - 4 = 5. Every earlier position where the prefix sum was 5 defines a valid subarray from j to i-1. If the prefix sum of 5 appeared 3 times earlier, there are 3 valid subarrays ending at i.

A hash map that stores {prefix_sum → how many times it appeared} answers this in O(1).


The one idea to remember

The entire pattern in one sentence

As you scan left to right, keep a running prefix sum and a frequency map; for each position, the number of valid subarrays ending here equals the number of times (prefix_sum - k) has appeared in the map before — then add the current prefix sum to the map.


Watch it happen, frame by frame

Array: [3, 4, 7, 2, -3, 1, 4, 2], k = 7.

We initialise freq = {0: 1} — there is one "empty prefix" of sum 0 (before the array starts).

i=0: prefix=3.  need=3-7=-4.  freq[-4]=0.  count+=0=0.  freq={0:1, 3:1}
i=1: prefix=7.  need=7-7=0.   freq[0]=1.   count+=1=1.  freq={0:1, 3:1, 7:1}
     → subarray [3,4] (indices 0..1) ✓
i=2: prefix=14. need=14-7=7.  freq[7]=1.   count+=1=2.  freq={0:1, 3:1, 7:1, 14:1}
     → subarray [4,7] (indices 1..2) ✓
i=3: prefix=16. need=16-7=9.  freq[9]=0.   count+=0=2.  freq={..., 16:1}
i=4: prefix=13. need=13-7=6.  freq[6]=0.   count+=0=2.  freq={..., 13:1}
i=5: prefix=14. need=14-7=7.  freq[7]=1.   count+=1=3.  freq={..., 14:2}
     → subarray [7,2,-3,1] (indices 2..5) ✓
i=6: prefix=18. need=18-7=11. freq[11]=0.  count+=0=3.  freq={..., 18:1}
i=7: prefix=20. need=20-7=13. freq[13]=1.  count+=1=4.  freq={..., 20:1}
     → subarray [2,-3,1,4,2] ... wait, let me verify: 2+(-3)+1+4+2=6. Hmm.

Let me retrace i=7: prefix=20, indices 0..7 sum = 3+4+7+2-3+1+4+2 = 20. We need prefix[j]=13. At i=4, prefix was 13. That means subarray from index 5..7: arr[5]+arr[6]+arr[7] = 1+4+2 = 7. ✓

Final count = 4.
Subarrays summing to 7: [3,4], [4,7], [7,2,-3,1], [1,4,2]

Pause & think

Cover the trace below. Try [1, -1, 1] with k = 0. Initialise freq = {0: 1}. What is count at the end?

Check your trace
i=0: prefix=1.  need=1-0=1.  freq[1]=0.  count=0.  freq={0:1, 1:1}
i=1: prefix=0.  need=0-0=0.  freq[0]=1.  count=1.  freq={0:2, 1:1}
     → subarray [1,-1] (indices 0..1) ✓
i=2: prefix=1.  need=1-0=1.  freq[1]=1.  count=2.  freq={0:2, 1:2}
     → subarray [-1,1] (indices 1..2) ✓

Final count = 2. ✅
(Note: entire array [1,-1,1] sums to 1, not 0, so it doesn't count.)

Now, the code — line by line

from collections import defaultdict

def subarraySum(nums, k):
    freq = defaultdict(int)
    freq[0] = 1        # the empty prefix (before index 0) has sum 0 — count it

    prefix = 0
    count = 0

    for num in nums:
        prefix += num                  # extend the running total by one element

        need = prefix - k              # what earlier prefix sum would close a valid subarray?
        count += freq[need]            # how many times did that prefix appear before?

        freq[prefix] += 1              # record that we've now seen this prefix sum one more time
                                       # (AFTER querying, so we don't count the current position as "earlier")

    return count

Mapping every line to the hiker's journal:

  • freq = {0: 1} — before the hike began, the total was 0. That "page 0" counts.
  • prefix += num — update the running altitude after one hour.
  • need = prefix - k — what total would an earlier page need to show for this window to sum to k?
  • count += freq[need] — look up how many earlier pages showed that total.
  • freq[prefix] += 1 — file this page in the journal before moving on. Must happen after the query — otherwise we'd count the current position as an "earlier" position, creating fake zero-length subarrays.

Why does initialising freq[0] = 1 matter?

Suppose the entire prefix from index 0 to i sums to exactly k. Then prefix - k = 0, and we need freq[0] to be 1 to count that subarray. Without the initialisation, freq[0] = 0 and we'd miss all subarrays that start from the very beginning.

Think of it as the hiker's journal having a "page 0" that records the starting altitude — it makes the formula work uniformly for all subarrays, including those starting at index 0.


Why is it always correct?

For every pair (j, i) where prefix[i] - prefix[j] = k:

  • prefix[j] gets stored in freq when we process index j.
  • When we process index i, we query freq[prefix[i] - k] = freq[prefix[j]] and find it.
  • We add 1 to count for that pair.

Every valid pair is counted exactly once. No pair is missed (we store every prefix). No pair is double-counted (we query before storing the current prefix).

Time: O(n) — one pass, O(1) hash map operations per element. Space: O(n) — the hash map stores at most n+1 distinct prefix sums.


When should I reach for this? (the trigger list)

Reach for prefix-sum + hash map counting when:

  • You need to count subarrays (not just find one) satisfying a sum condition.
  • The array has negative numbers (ruling out the simple variable-window sliding approach, which only works on positive values).
  • The condition is sum == k, sum % k == 0, XOR == k, or similar equality on a prefix aggregate.
  • The brute-force answer is O(n²) nested loops over all (i, j) pairs.

One-sentence test: "Can I express the subarray condition as prefix[i] - prefix[j] = something fixed?" If yes — hash map the prefix sums.


The same trick in four disguises

Disguise 1 — Subarray Sum Divisible by K (LC #974)

Instead of prefix[i] - prefix[j] == k, we want (prefix[i] - prefix[j]) % k == 0, i.e., prefix[i] % k == prefix[j] % k. Store remainders (not raw sums) in the hash map.

def subarraysDivByK(nums, k):
    freq = defaultdict(int)
    freq[0] = 1
    prefix = 0
    count = 0
    for num in nums:
        prefix = (prefix + num) % k
        if prefix < 0: prefix += k    # Python % is always non-negative, but explicit guard
        count += freq[prefix]
        freq[prefix] += 1
    return count

Same skeleton — only the "key" in the map changes from raw prefix to remainder.

Disguise 2 — Count Subarrays with XOR Equal to K

Same pattern, but replace + with ^ and prefix - k with prefix ^ k:

def countSubarraysXOR(nums, k):
    freq = defaultdict(int)
    freq[0] = 1
    prefix = 0
    count = 0
    for num in nums:
        prefix ^= num                  # running XOR instead of sum
        count += freq[prefix ^ k]      # XOR equivalent of "prefix - k"
        freq[prefix] += 1
    return count

Because XOR is self-inverse: prefix ^ k ^ k = prefix, so prefix_j = prefix_i ^ k is the condition.

Disguise 3 — Longest Subarray with Sum k

Instead of counting, track the first time each prefix sum appeared. The longest subarray ending at i with sum k ends at i and starts at first_seen[prefix - k] + 1.

def longestSubarraySum(nums, k):
    first_seen = {0: -1}               # prefix 0 seen "before" index 0
    prefix = 0
    max_len = 0
    for i, num in enumerate(nums):
        prefix += num
        if prefix - k in first_seen:
            max_len = max(max_len, i - first_seen[prefix - k])
        if prefix not in first_seen:
            first_seen[prefix] = i     # store only first occurrence
    return max_len

Key change: store first_seen (not frequency); only record if not seen before (for maximum length).

Level up — Count subarrays with equal 0s and 1s (LC #525)

Replace every 0 with -1. Now "equal 0s and 1s" becomes "subarray sum equals 0" — the exact standard problem. Apply the exact same algorithm with k = 0.

def findMaxLength(nums):
    # treat 0 as -1, count subarrays with sum 0
    freq = {0: 1}
    prefix = 0
    count = 0
    for num in nums:
        prefix += 1 if num == 1 else -1
        count += freq.get(prefix, 0)   # subarrays with equal 0s and 1s ending here
        freq[prefix] = freq.get(prefix, 0) + 1
    return count

The encoding trick (0 → -1) is the whole insight. Once encoded, the code is identical.


Traps that catch beginners

Watch out for these

  • Forgetting freq[0] = 1 initialisation. Without it, subarrays that start from index 0 are never counted. This is the most common bug.
  • Querying after storing (wrong order). If you do freq[prefix] += 1 before count += freq[prefix - k], you might count a "subarray" from position i back to itself — a zero-length window with sum 0, not a real subarray.
  • Using this when the array is guaranteed non-negative and k > 0. In that case the variable sliding window (pattern 1.05) is simpler and also O(n). Prefix-sum counting is for arrays with negatives or when you want to count (not just find) subarrays.
  • Mixing up longest vs count. For "count," use a frequency map. For "longest," use a first-seen-index map and store only the first occurrence of each prefix.
BugFix
Missing freq[0] = 1Always initialise it — handles subarrays starting at index 0
freq[prefix] += 1 before queryingAlways query first, then store
Giving wrong answer on negative arraysPrefix-sum counting handles negatives perfectly; sliding window does not

Say it like a pro (interview one-liner)

"The key insight is that a subarray arr[j..i] sums to k exactly when prefix[i] - prefix[j] = k, i.e., prefix[j] = prefix[i] - k. So I maintain a running prefix sum and a hash map of how many times each prefix has appeared. For each new prefix, I look up prefix - k in the map — that's the count of valid subarrays ending here. O(n) time, O(n) space, single pass."


Remember this forever

Subarray Counting — Prefix Sum + Hash Map

prefix[i] - prefix[j] = k → we need prefix[j] = prefix[i] - k.

Initialise freq = {0: 1}. For each element: update prefix → query freq[prefix - k] → store current prefix.


Trigger: count subarrays with sum/XOR/mod condition · negative numbers allowed

Key habit: query before storing · always init freq[0] = 1

Cost: O(n) time, O(n) space

For longest (not count): use first-seen index map; store only first occurrence


Check yourself

Why do we initialise freq[0] = 1 before the loop?

It represents the "empty prefix" before index 0 — a prefix of sum 0. When the running prefix at some position i equals k, then prefix - k = 0, and we need freq[0] = 1 to count the subarray from index 0 to i. Without it, every subarray starting at index 0 is missed.

Why must we query freq before updating it with the current prefix?

If we stored first, then queried, we'd be counting the current position as a valid "earlier" position. For example, if prefix = k exactly, we'd count a fake subarray from position i back to itself — a zero-length window. Querying first ensures we only count strictly earlier occurrences.

The sliding window also counts subarrays in O(n). Why do we need this pattern at all?

The variable sliding window (pattern 1.05) only works correctly when all values are non-negative (so shrinking the window always decreases the sum). With negative numbers in the array, shrinking the window can accidentally increase the sum — the monotone invariant breaks. Prefix-sum counting works correctly regardless of signs, making it the only O(n) approach for the general case.

How does the XOR version differ from the sum version?

Replace sum with XOR throughout: prefix ^= num instead of prefix += num, and freq[prefix ^ k] instead of freq[prefix - k]. This works because XOR is self-inverse: if prefix_i ^ prefix_j = k, then prefix_j = prefix_i ^ k. Every other structural detail — frequency map, initialise freq[0]=1, query-then-store — is identical.


Practice problems

ProblemDifficultyWhat to noticeLink
Subarray Sum Equals KMediumCore pattern — handle negatives with prefix hash mapLC #560
Contiguous Array (equal 0s and 1s)MediumEncode 0→-1, then find subarrays summing to 0LC #525
Subarray Sums Divisible by KMediumStore remainders prefix % k; same frequency mapLC #974
Count of Subarrays with XOR Equal KMediumReplace + with ^; replace prefix-k with prefix^kLC #1542 related
Longest Subarray Sum Equals KMediumSwitch to first-seen index map; track max lengthGFG variant

That completes the entire Arrays & Two Pointers chapter (patterns 1.01–1.17). You now have the full toolkit for the most common array-manipulation questions in technical interviews.

Next up: Chapter 2 — Hashing, starting with Two Sum Pattern — where a hash map turns the classic O(n²) pair search into a single O(n) pass.