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.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why does initialising freq[0] = 1 matter?
- Why is it always correct?
- When should I reach for this? (the trigger list)
- The same trick in four disguises
- Disguise 1 — Subarray Sum Divisible by K (LC #974)
- Disguise 2 — Count Subarrays with XOR Equal to K
- Disguise 3 — Longest Subarray with Sum k
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
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 exactlyk.
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 tok?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 infreqwhen we process indexj.- When we process index
i, we queryfreq[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] = 1initialisation. 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] += 1beforecount += 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.
| Bug | Fix |
|---|---|
Missing freq[0] = 1 | Always initialise it — handles subarrays starting at index 0 |
freq[prefix] += 1 before querying | Always query first, then store |
| Giving wrong answer on negative arrays | Prefix-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 tokexactly whenprefix[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 upprefix - kin 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
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Subarray Sum Equals K | Medium | Core pattern — handle negatives with prefix hash map | LC #560 |
| Contiguous Array (equal 0s and 1s) | Medium | Encode 0→-1, then find subarrays summing to 0 | LC #525 |
| Subarray Sums Divisible by K | Medium | Store remainders prefix % k; same frequency map | LC #974 |
| Count of Subarrays with XOR Equal K | Medium | Replace + with ^; replace prefix-k with prefix^k | LC #1542 related |
| Longest Subarray Sum Equals K | Medium | Switch to first-seen index map; track max length | GFG 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.