Prefix Sum with HashMap
Sliding window fails the moment an array can contain negative numbers: growing the window no longer grows the sum, so “shrink while too big” stops making sense. It also fails when a problem asks you to count subarrays rather than find the best one.
The pattern that handles both is prefix sums plus a hash map. It rests on one identity:
Rearranged, a subarray ending at jj sums to kk exactly when some earlier
prefix equals . So instead of searching for
subarrays, you count prefix values you have already seen — one pass,
one dict, .
What you’ll learn
- The prefix-sum identity, and the rearrangement that turns it into a lookup.
- Why the map must be seeded with
{0: 1}{0: 1}, and what breaks without it. - Counting occurrences vs. remembering the earliest index — the two flavours, and which each problem needs.
- The remainder and balance transforms that make this pattern cover far more than plain sums.
- Three real LeetCode problems solved in the browser: 560, 974, 525.
The cue
The template
Two flavours. Which one you need depends on whether the answer is a count or a length.
# FLAVOUR 1 -- COUNT subarrays. Map: prefix value -> how many times seen.
def count_subarrays_with_sum(nums, k):
seen = {0: 1} # one empty prefix, so subarrays starting at index 0 count
running = 0
count = 0
for x in nums:
running += x
count += seen.get(running - k, 0) # every earlier match is a subarray
seen[running] = seen.get(running, 0) + 1
return count
# FLAVOUR 2 -- LONGEST subarray. Map: prefix value -> EARLIEST index seen.
def longest_subarray_with_sum(nums, k):
first = {0: -1} # empty prefix sits "before" index 0
running = 0
best = 0
for i, x in enumerate(nums):
running += x
if running - k in first:
best = max(best, i - first[running - k])
if running not in first: # only record the FIRST occurrence
first[running] = i
return best
print(count_subarrays_with_sum([1, 1, 1], 2)) # 2
print(longest_subarray_with_sum([1, -1, 5, -2, 3], 3)) # 4# FLAVOUR 1 -- COUNT subarrays. Map: prefix value -> how many times seen.
def count_subarrays_with_sum(nums, k):
seen = {0: 1} # one empty prefix, so subarrays starting at index 0 count
running = 0
count = 0
for x in nums:
running += x
count += seen.get(running - k, 0) # every earlier match is a subarray
seen[running] = seen.get(running, 0) + 1
return count
# FLAVOUR 2 -- LONGEST subarray. Map: prefix value -> EARLIEST index seen.
def longest_subarray_with_sum(nums, k):
first = {0: -1} # empty prefix sits "before" index 0
running = 0
best = 0
for i, x in enumerate(nums):
running += x
if running - k in first:
best = max(best, i - first[running - k])
if running not in first: # only record the FIRST occurrence
first[running] = i
return best
print(count_subarrays_with_sum([1, 1, 1], 2)) # 2
print(longest_subarray_with_sum([1, -1, 5, -2, 3], 3)) # 4How it works
Take nums = [3, 4, 7, 2, -3, 1, 4, 2]nums = [3, 4, 7, 2, -3, 1, 4, 2], k = 7k = 7:
ii | nums[i]nums[i] | runningrunning | running - krunning - k | seen before? | count |
|---|---|---|---|---|---|
| 0 | 3 | 3 | -4 | no | 0 |
| 1 | 4 | 7 | 0 | yes (seed) | 1 |
| 2 | 7 | 14 | 7 | yes | 2 |
| 3 | 2 | 16 | 9 | no | 2 |
| 4 | -3 | 13 | 6 | no | 2 |
| 5 | 1 | 14 | 7 | yes | 3 |
| 6 | 4 | 18 | 11 | no | 3 |
| 7 | 2 | 20 | 13 | yes | 4 |
Four subarrays: [3,4][3,4], [7][7], [7,2,-3,1][7,2,-3,1], [1,4,2][1,4,2]. Notice the seed
firing at i = 1i = 1 — that is the subarray starting at index 00, and it is
exactly what a missing {0: 1}{0: 1} would lose.
| Time | Space | |
|---|---|---|
| Brute force over all subarrays | ||
| Prefix sums + hash map |
The variant map
The power of this pattern is that you can transform the array first, turning an unrelated-looking condition into “sum equals a target”.
| Variant | The transform | Canonical problem |
|---|---|---|
| Sum equals k | None | 560 Subarray Sum Equals K |
| Sum divisible by k | Key on running % krunning % k instead of runningrunning | 974 · 523 |
| Equal count of two symbols | Map one to +1+1, the other to -1-1; look for sum 00 | 525 Contiguous Array |
| Exactly k odd numbers | Map odd to 11, even to 00; look for sum kk | 1248 Nice Subarrays |
| Count on a tree path | Same map, but undo the entry when leaving the node | 437 Path Sum III |
| 2D submatrix sums | Fix a row pair, collapse columns to 1D, apply the template | 1074 |
Practice — real LeetCode problems
LC 560 — Subarray Sum Equals K · Medium
Problem. Given an integer array numsnums and an integer kk, return the
total number of contiguous subarrays whose sum equals kk.
Constraints. 1 <= len(nums) <= 2 * 10^41 <= len(nums) <= 2 * 10^4,
-1000 <= nums[i] <= 1000-1000 <= nums[i] <= 1000, -10^7 <= k <= 10^7-10^7 <= k <= 10^7. Note the negatives —
this is why it is not a sliding window.
Examples. nums = [1,1,1], k = 2nums = [1,1,1], k = 2 gives 22 ·
nums = [1,2,3], k = 3nums = [1,2,3], k = 3 gives 22 · nums = [1,-1,0], k = 0nums = [1,-1,0], k = 0 gives 33
Editorial — approach, complexity, follow-ups
A subarray ending at the current index sums to kk exactly when some
earlier prefix equals running - krunning - k. So the number of such subarrays is
the number of times running - krunning - k has been seen — which is what the map
stores.
Time . Space .
Note the answer counts subarrays, so equal prefix values must be tallied,
not overwritten — [1,-1,0][1,-1,0] reaches prefix 00 three times and each
pairing is a distinct valid subarray.
Follow-ups you should expect: “Longest such subarray instead?” — switch to flavour 2, storing earliest indices. “All values positive?” — then a sliding window works and gives space; say so, it shows you know the trade. “Do it on a binary tree?” — LC 437: same map, but decrement the entry as you unwind so only the current root-to-node path is in scope.
LC 974 — Subarray Sums Divisible by K · Medium
Problem. Return the number of contiguous subarrays whose sum is
divisible by kk.
Constraints. 1 <= len(nums) <= 3 * 10^41 <= len(nums) <= 3 * 10^4,
-10^4 <= nums[i] <= 10^4-10^4 <= nums[i] <= 10^4, 2 <= k <= 10^42 <= k <= 10^4.
Examples. nums = [4,5,0,-2,-3,1], k = 5nums = [4,5,0,-2,-3,1], k = 5 gives 77 ·
nums = [5], k = 9nums = [5], k = 9 gives 00 · nums = [-1,2,9], k = 2nums = [-1,2,9], k = 2 gives 22
Editorial — approach, complexity, follow-ups
is divisible by kk exactly when
. So instead of
looking up running - krunning - k, look up runningrunning itself — among prefixes
already reduced mod kk.
Time . Space — only kk distinct remainders exist,
which is tighter than the of LC 560.
Follow-ups you should expect: “Is there a subarray of length ≥ 2 whose
sum is divisible by kk?” — LC 523, same idea plus an index check for the
length constraint. “Why is space not ?” — because remainders
live in [0, k)[0, k), so a kk-slot list beats a dict here.
LC 525 — Contiguous Array · Medium
Problem. Given a binary array numsnums, return the length of the longest
contiguous subarray with an equal number of 00s and 11s.
Constraints. 1 <= len(nums) <= 10^51 <= len(nums) <= 10^5, nums[i]nums[i] is 00 or 11.
Examples. nums = [0,1]nums = [0,1] gives 22 · nums = [0,1,0]nums = [0,1,0] gives 22 ·
nums = [0,1,1,1,1,1,0,0,0]nums = [0,1,1,1,1,1,0,0,0] gives 66 · nums = [1,1,1]nums = [1,1,1] gives 00
Editorial — approach, complexity, follow-ups
Map 0 -> -10 -> -1 and 1 -> +11 -> +1. A subarray has equal counts exactly when its
mapped sum is 00, i.e. when two prefixes share the same running balance.
The longest such subarray for a given balance spans from its earliest
occurrence to the current index.
Time . Space .
[1,1,1][1,1,1] returning 00 is the “no valid subarray” case — the balance
never repeats, so bestbest stays at its initial 00.
Follow-ups you should expect: “Equal counts of three symbols?” — key
the map on a tuple of differences, e.g. (count_a - count_b, count_b - count_c)(count_a - count_b, count_b - count_c). “Return the subarray itself?” — also store the index where
bestbest was set. “Equal counts of any two values in a non-binary array?”
— same idea, one map per pair of interest.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 560 | Subarray Sum Equals K | Medium | The base counting template; negatives rule out a window |
| 974 | Subarray Sums Divisible by K | Medium | Key on running % krunning % k; space drops to |
| 523 | Continuous Subarray Sum | Medium | Remainders plus a “length at least 2” index check |
| 525 | Contiguous Array | Medium | The +1 / -1+1 / -1 balance transform; earliest index |
| 930 | Binary Subarrays With Sum | Medium | Counting with a binary array — or atMost(k) - atMost(k-1)atMost(k) - atMost(k-1) |
| 1248 | Count Number of Nice Subarrays | Medium | Map odd to 11, even to 00, then count sum == k== k |
| 437 | Path Sum III | Medium | The same map on a tree path — decrement on the way back up |
| 1074 | Number of Submatrices That Sum to Target | Hard | Fix a row pair, collapse to 1D, then run LC 560 |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Why not a sliding window?” | Whether you know each pattern’s limits | Negative values break the window’s monotonic sum; and windows find extremes, not counts |
“Why seed with {0: 1}{0: 1}?” | Whether you derived it or copied it | It represents the empty prefix, so subarrays starting at index 00 are counted |
| “Count vs. earliest index?” | Whether you match the map to the question | Counting a total means tally every occurrence; measuring a length means keep only the first |
| “What if all values are positive?” | Trade-off awareness | A window then works and needs space instead of |
| “Space is — can you do better?” | Precision | For the divisibility variant, yes: , since only kk remainders exist |
| “Extend to 2D” | Composition | Fix a pair of rows, collapse each column into a running sum, and run the 1D template — |
Edge-case checklist
k = 0k = 0— forces the count-before-record ordering;[1,-1,0][1,-1,0]is the test.- Negative numbers — the entire reason for this pattern; make sure nothing in your solution assumes monotonicity.
- Zeros in the array — create repeated prefix values and therefore extra valid subarrays; they must be tallied, not deduplicated.
- Whole array is the answer — verifies the seed entry works.
- No valid subarray — must return
00, not-1-1orfloat("inf")float("inf"). - Single element —
[5], k = 5[5], k = 5gives11;[5], k = 9[5], k = 9gives00. - Overflow — irrelevant in Python (arbitrary-precision ints), but worth naming if the interviewer thinks in C++ or Java.
- Earliest-index guard (length variants) — never overwrite a balance already in the map.
Recap
- , rearranged
into a lookup: a subarray ending here hits the target when
running - krunning - khas been seen before. - Two flavours: count occurrences (
{0: 1}{0: 1}) for “how many”, earliest index ({0: -1}{0: -1}) for “how long”. - Seed the map, and for length variants never overwrite an existing key.
- Count before recording so
k = 0k = 0cannot match a zero-length subarray. - Transform first, then apply the template:
% k% kfor divisibility,+1/-1+1/-1for equal counts,odd/evenodd/evenfor parity counts. That is what makes one small template cover a dozen problems. - Choose this over a window whenever values can be negative, or the question asks for a count.
Next: Kadane and Maximum Subarray — the other classic answer to “contiguous subarray with negatives”, when you want the best sum rather than a count.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
