Skip to content

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:

sum(i..j)=prefix[j]prefix[i1]\text{sum}(i..j) = \text{prefix}[j] - \text{prefix}[i-1]

Rearranged, a subarray ending at jj sums to kk exactly when some earlier prefix equals prefix[j]k\text{prefix}[j] - k. So instead of searching for subarrays, you count prefix values you have already seen — one pass, one dict, O(n)O(n).

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.

prefix_sum_hashmap_template.py
# 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
prefix_sum_hashmap_template.py
# 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

How it works

Take nums = [3, 4, 7, 2, -3, 1, 4, 2]nums = [3, 4, 7, 2, -3, 1, 4, 2], k = 7k = 7:

iinums[i]nums[i]runningrunningrunning - krunning - kseen before?count
033-4no0
1470yes (seed)1
27147yes2
32169no2
4-3136no2
51147yes3
641811no3
722013yes4

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.

TimeSpace
Brute force over all subarraysO(n2)O(n^2)O(1)O(1)
Prefix sums + hash mapO(n)O(n)O(n)O(n)

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”.

VariantThe transformCanonical problem
Sum equals kNone560 Subarray Sum Equals K
Sum divisible by kKey on running % krunning % k instead of runningrunning974 · 523
Equal count of two symbolsMap one to +1+1, the other to -1-1; look for sum 00525 Contiguous Array
Exactly k odd numbersMap odd to 11, even to 00; look for sum kk1248 Nice Subarrays
Count on a tree pathSame map, but undo the entry when leaving the node437 Path Sum III
2D submatrix sumsFix a row pair, collapse columns to 1D, apply the template1074

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 O(n)O(n). Space O(n)O(n).

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 O(1)O(1) 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

sum(i..j)\text{sum}(i..j) is divisible by kk exactly when prefix[j]prefix[i1](modk)\text{prefix}[j] \equiv \text{prefix}[i-1] \pmod{k}. So instead of looking up running - krunning - k, look up runningrunning itself — among prefixes already reduced mod kk.

Time O(n)O(n). Space O(k)O(k) — only kk distinct remainders exist, which is tighter than the O(n)O(n) 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 O(k)O(k) not O(n)O(n)?” — 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 O(n)O(n). Space O(n)O(n).

[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

#ProblemDifficultyThe twist
560Subarray Sum Equals KMediumThe base counting template; negatives rule out a window
974Subarray Sums Divisible by KMediumKey on running % krunning % k; space drops to O(k)O(k)
523Continuous Subarray SumMediumRemainders plus a “length at least 2” index check
525Contiguous ArrayMediumThe +1 / -1+1 / -1 balance transform; earliest index
930Binary Subarrays With SumMediumCounting with a binary array — or atMost(k) - atMost(k-1)atMost(k) - atMost(k-1)
1248Count Number of Nice SubarraysMediumMap odd to 11, even to 00, then count sum == k== k
437Path Sum IIIMediumThe same map on a tree path — decrement on the way back up
1074Number of Submatrices That Sum to TargetHardFix a row pair, collapse to 1D, then run LC 560

Interview follow-ups

They askWhat they’re checkingThe answer
“Why not a sliding window?”Whether you know each pattern’s limitsNegative 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 itIt represents the empty prefix, so subarrays starting at index 00 are counted
“Count vs. earliest index?”Whether you match the map to the questionCounting a total means tally every occurrence; measuring a length means keep only the first
“What if all values are positive?”Trade-off awarenessA window then works and needs O(1)O(1) space instead of O(n)O(n)
“Space is O(n)O(n) — can you do better?”PrecisionFor the divisibility variant, yes: O(k)O(k), since only kk remainders exist
“Extend to 2D”CompositionFix a pair of rows, collapse each column into a running sum, and run the 1D template — O(rows2cols)O(rows^2 \cdot cols)

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-1 or float("inf")float("inf").
  • Single element[5], k = 5[5], k = 5 gives 11; [5], k = 9[5], k = 9 gives 00.
  • 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

  • sum(i..j)=prefix[j]prefix[i1]\text{sum}(i..j) = \text{prefix}[j] - \text{prefix}[i-1], rearranged into a lookup: a subarray ending here hits the target when running - krunning - k has 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 = 0 cannot match a zero-length subarray.
  • Transform first, then apply the template: % k% k for divisibility, +1/-1+1/-1 for equal counts, odd/evenodd/even for 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 coffee

Was this page helpful?

Let us know how we did