Builds on Hash Map and Prefix Sums, which you haven’t finished yet.
Prefix Sum + Hash Map
You have an array of integers nums and a target integer k. Count how many contiguous, non-empty subarrays of nums add up to exactly k, and return that count.
[1,1] at positions 0-1 and [1,1] at positions 1-2 both sum to 2.Prefix Sum Map
prefix[R+1] - prefix[L] = target (Algebraic Point Lookup)| 1 | from typing import Callable, Hashable, List |
| 2 | from collections import defaultdict |
| 3 | Â |
| 4 | Â |
| 5 | def count_by_prefix(nums: List[int], key: Callable[[int], Hashable], needed_key: Callable[[int], Hashable]) -> int: |
| 6 | """Prefix Sum Map: count the subarrays nums[L..R] where key(P[L]) == needed_key(P[R + 1]). |
| 7 | P[i] is the sum of the first i values, so P[0] = 0 is the empty prefix. |
| 8 | The subarray nums[L..R] sums to P[R + 1] - P[L]. |
| 9 | For an exact sum k, key(p) = p and needed_key(p) = p - k. |
| 10 | For a sum divisible by k, key(p) = needed_key(p) = p % k. |
| 11 | A longest subarray (Contiguous Array) stores the first index of each key instead of a count: see that gotcha. |
| 12 | N = len(nums). T = the cost of one key or needed_key call. |
| 13 | Time: O(N * T) | Space: O(N) |
| 14 | """ |
| 15 | # prefix_counts: how many earlier prefixes (keyed by key()) have occurred so far |
| 16 | prefix_counts = defaultdict(int) |
| 17 | prefix_counts[key(0)] = 1 # The empty prefix P[0] = 0 occurs once |
| 18 | Â |
| 19 | running_sum = 0 |
| 20 | total_matches = 0 |
| 21 | Â |
| 22 | for num in nums: |
| 23 | # 1. Extend The Running Prefix: |
| 24 | # running_sum becomes P[R + 1] once num is folded in. |
| 25 | running_sum += num |
| 26 | # 2. Look Up The Matching Earlier Prefix: |
| 27 | # Every earlier prefix P[L] already counted under needed_key(running_sum) ends a subarray nums[L..R] that matches, so add how many there were before this prefix is added. |
| 28 | total_matches += prefix_counts.get(needed_key(running_sum), 0) |
| 29 | # 3. Record The Current Prefix: |
| 30 | # Add running_sum after the lookup, so a subarray never matches against itself. |
| 31 | prefix_counts[key(running_sum)] += 1 |
| 32 | Â |
| 33 | return total_matches |
num is added, prefix_counts holds how often each prefix sum of the elements before it has occurred, including the empty prefix 0.The sum of nums[L..R] equals P[R+1] - P[L], where P is the running prefix sum. So the number of subarrays ending here with sum k is how many earlier prefixes equal running_sum - k: look up needed_prefix in prefix_counts, then add the current running_sum. Each subarray is counted once, at its last element, and nothing depends on the values being positive, so it works with negative numbers, where sliding windows fail. The trap is the empty prefix: start with prefix_counts[0] = 1, or every subarray that begins at index 0 is missed.
See it run
Prefix Sum Map, step by step
running_sum 0prefix_counts {}total_matches 0
Row nums holds the input. Row prefix holds the prefix sums, where prefix[0] = 0 is the empty prefix. Row needed holds running_sum - k.
numthe value nums[i] added to running_sum in this steprunning_sumthe prefix sum that ends at the current valuestoreda prefix sum written into prefix_counts in this stepneededthe value running_sum - k that is looked up in prefix_countsmatchan earlier prefix sum equal to needed_prefixempty prefixthe empty prefix, with sum 0, that starts the map
Start: nums = [1, 2, 3, -3, 3] and k = 3. The map must know the empty prefix before the loop starts.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
prefix_counts[0] = 1Mistakeprefix_counts = defaultdict(int)Fixprefix_counts = defaultdict(int) prefix_counts[0] = 1What happens: A subarray that starts at index 0 has the sum
running_sum. So itsneeded_prefixis 0. Prefix sum 0 belongs to the empty prefix, before any element. Withoutprefix_counts[0] = 1, that lookup finds nothing, and those subarrays are not counted.Example: nums = [1, 1, 1], k = 2Mistake: returns 1Fix: returns 2
- All values are positive and you want the shortest subarray whose sum is at least a target, as in Minimum Size Subarray Sum (LC 209), not a count of exact sums. See Shortest Window
- The input is a list of updates that each add to a whole range, and you need every final value, as in Range Addition (LC 370), not subarrays with a given sum. See Difference Array
Deep Dive
From a search over ranges to a lookup
The Prefix Sums card answers a question about a range you already know. Here the ranges are unknown. You must count the ranges nums[L..R] whose sum is k.
The range sum is P[R + 1] - P[L]. It equals k exactly when P[L] = P[R + 1] - k. So at each element, the question is how many earlier prefixes have the value running_sum - k. That value is needed_prefix.
A hash map answers the question in constant time. prefix_counts[v] is the number of earlier prefixes with the value v. The loop adds prefix_counts[needed_prefix] to total_matches, and it counts each subarray once, at its last element.
The map keeps a count and not a yes or no, because several earlier prefixes can have the same value. Each of them is the start of a different subarray.
Nothing in this argument uses the sign of a value. It works with negative numbers. A shrinking window does not: for nums = [1, -1, 1] and k = 1, a window that shrinks when the sum is above k finds 2 subarrays, but the answer is 3.
The loop traced on [1, 2, 3, -3, 3] with k = 3
The prefix sums are 0, 1, 3, 6, 3, 6. The map starts as {0: 1}, because the empty prefix occurs once.
At nums[1], running_sum = 3 and needed_prefix = 0. The map holds 0 once. That match is the subarray [1, 2], which starts at index 0.
At nums[2], running_sum = 6 and needed_prefix = 3. The prefix 3 occurred once, so the subarray [3] is counted.
At nums[3] = -3, running_sum returns to 3 and needed_prefix = 0. The subarray [1, 2, 3, -3] is counted. Then prefix_counts[3] becomes 2.
At nums[4], running_sum = 6 and needed_prefix = 3. The map holds 3 twice, so two subarrays are counted: [3, -3, 3] and [3]. The total is 5.
The trap is the first line of the code. Without prefix_counts[0] = 1, the matches that use the empty prefix are missed. Here that loses [1, 2] and [1, 2, 3, -3], and the answer would be 3.
Order of the lookup and the record
The loop looks up needed_prefix first and records running_sum after. The lookup must see only earlier prefixes.
If the record comes first and k = 0, then needed_prefix equals running_sum. The lookup finds the current prefix itself and counts an empty subarray. For nums = [1] and k = 0, the answer would be 1 instead of 0.
Changing the key for other questions
The template takes a key and a needed_key. Only these two change between problems. The loop stays the same.
For a sum divisible by k, as in Subarray Sums Divisible by K (LC 974), two prefixes with the same remainder P % k enclose a sum divisible by k. The key is the remainder. For nums = [4, 5, 0, -2, -3, 1] and k = 5, the answer is 7.
For the longest subarray, as in Maximum Size Subarray Sum Equals k (LC 325), store the first index of each prefix and not a count. The longest subarray that ends at i starts right after the first index of running_sum - k. For nums = [1, -1, 5, -2, 3] and k = 3, the answer is 4.
For equal numbers of 0 and 1, as in Contiguous Array (LC 525), replace each 0 by -1. A subarray with equal numbers now has the sum 0. For [0, 1, 0], the answer is 2.
The cost of every version is O(N) time. The map holds up to N + 1 prefixes, so the extra space is O(N).
60-Second Pitch
"I need the number of subarrays with sum k, and values can be negative, so a window will not work. A subarray from L to R has the sum P[R + 1] - P[L]. It equals k when P[L] equals running_sum - k. So at each element I look up how many earlier prefixes have that value. I keep the counts in a hash map and start with prefix_counts[0] = 1 for the empty prefix. I look up first and record after, so a subarray never matches itself. The time is O(N) and the space is O(N). For divisibility the key becomes the remainder, and for the longest subarray the map stores the first index."
Common Questions
- Why does a sliding window fail here, and this method does not?
A window moves by a rule such as "shrink while the sum is too big". That rule needs the sum to grow when the window grows, which fails with negative values. The map method only uses the identity
P[R + 1] - P[L] = k, which holds for any values.- Why does the map store counts and not just which prefixes exist?
Several earlier prefixes can have the same value. Each one starts a different subarray that ends at the current element. The count says how many.
- Why is
prefix_counts[0] = 1needed before the loop? A subarray that starts at index 0 has the sum
running_sum, so its needed prefix is 0. The prefix 0 is the empty prefix, before any element. The map must hold it, or those subarrays are missed.- How do you find the longest subarray and not the count?
Store the first index where each prefix sum appears, with
0at index-1. At indexi, ifrunning_sum - kis in the map, the length isiminus that first index. Never overwrite a stored index, because the first index gives the longest subarray.- What is the cost, and what does the map cost in space?
The time is
O(N)with an average constant cost per map operation. The map holds up toN + 1different prefixes, so the extra space isO(N). The Prefix Sums card needs the same space but answers only known ranges.
SWE Reasoning
Spot it
- The question counts or finds subarrays with an exact sum, or with a sum divisible by
k, or with equal numbers of two kinds of values. - Values can be negative, so a window that grows and shrinks cannot decide which way to move.
- The ranges are unknown. The target is known.
- Example problems: Subarray Sum Equals K (LC 560), Subarray Sums Divisible by K (LC 974), Contiguous Array (LC 525).
The invariant, step by step
- Before the loop,
prefix_counts[0] = 1: the empty prefix occurs once. - For each
num,running_sumbecomes the prefix sum that ends atnum. prefix_countsholds the earlier prefixes only, so the lookup ofneeded_prefix = running_sum - kcounts the subarrays that end atnumand sum tok.- Only then is
running_sumadded toprefix_counts, so a subarray never matches itself. - Every subarray ends at exactly one element, so
total_matchescounts each subarray once.
Traps
- Leaving out
prefix_counts[0] = 1. Every subarray that starts at index 0 is missed. For[1, 1, 1]andk = 2, the answer is 1 instead of 2. - Recording before the lookup. With
k = 0the lookup finds the current prefix and counts an empty subarray. - Using a set and not a count. Two earlier prefixes with the same value are two different subarrays, and a set counts them once.
- Keeping the last index for the longest subarray. The first index gives the longest one. A later index overwrites it and makes the answer shorter.
- Using the sum as the key for divisibility. The key is
running_sum % k.
Practice
Builds on Hash Map and Prefix Sums, which you haven’t finished yet.
Prefix Sum + Hash Map
You have an array of integers nums and a target integer k. Count how many contiguous, non-empty subarrays of nums add up to exactly k, and return that count.
[1,1] at positions 0-1 and [1,1] at positions 1-2 both sum to 2.Prefix Sum Map
prefix[R+1] - prefix[L] = target (Algebraic Point Lookup)| 1 | from typing import Callable, Hashable, List |
| 2 | from collections import defaultdict |
| 3 | Â |
| 4 | Â |
| 5 | def count_by_prefix(nums: List[int], key: Callable[[int], Hashable], needed_key: Callable[[int], Hashable]) -> int: |
| 6 | """Prefix Sum Map: count the subarrays nums[L..R] where key(P[L]) == needed_key(P[R + 1]). |
| 7 | P[i] is the sum of the first i values, so P[0] = 0 is the empty prefix. |
| 8 | The subarray nums[L..R] sums to P[R + 1] - P[L]. |
| 9 | For an exact sum k, key(p) = p and needed_key(p) = p - k. |
| 10 | For a sum divisible by k, key(p) = needed_key(p) = p % k. |
| 11 | A longest subarray (Contiguous Array) stores the first index of each key instead of a count: see that gotcha. |
| 12 | N = len(nums). T = the cost of one key or needed_key call. |
| 13 | Time: O(N * T) | Space: O(N) |
| 14 | """ |
| 15 | # prefix_counts: how many earlier prefixes (keyed by key()) have occurred so far |
| 16 | prefix_counts = defaultdict(int) |
| 17 | prefix_counts[key(0)] = 1 # The empty prefix P[0] = 0 occurs once |
| 18 | Â |
| 19 | running_sum = 0 |
| 20 | total_matches = 0 |
| 21 | Â |
| 22 | for num in nums: |
| 23 | # 1. Extend The Running Prefix: |
| 24 | # running_sum becomes P[R + 1] once num is folded in. |
| 25 | running_sum += num |
| 26 | # 2. Look Up The Matching Earlier Prefix: |
| 27 | # Every earlier prefix P[L] already counted under needed_key(running_sum) ends a subarray nums[L..R] that matches, so add how many there were before this prefix is added. |
| 28 | total_matches += prefix_counts.get(needed_key(running_sum), 0) |
| 29 | # 3. Record The Current Prefix: |
| 30 | # Add running_sum after the lookup, so a subarray never matches against itself. |
| 31 | prefix_counts[key(running_sum)] += 1 |
| 32 | Â |
| 33 | return total_matches |
num is added, prefix_counts holds how often each prefix sum of the elements before it has occurred, including the empty prefix 0.The sum of nums[L..R] equals P[R+1] - P[L], where P is the running prefix sum. So the number of subarrays ending here with sum k is how many earlier prefixes equal running_sum - k: look up needed_prefix in prefix_counts, then add the current running_sum. Each subarray is counted once, at its last element, and nothing depends on the values being positive, so it works with negative numbers, where sliding windows fail. The trap is the empty prefix: start with prefix_counts[0] = 1, or every subarray that begins at index 0 is missed.
See it run
Prefix Sum Map, step by step
running_sum 0prefix_counts {}total_matches 0
Row nums holds the input. Row prefix holds the prefix sums, where prefix[0] = 0 is the empty prefix. Row needed holds running_sum - k.
numthe value nums[i] added to running_sum in this steprunning_sumthe prefix sum that ends at the current valuestoreda prefix sum written into prefix_counts in this stepneededthe value running_sum - k that is looked up in prefix_countsmatchan earlier prefix sum equal to needed_prefixempty prefixthe empty prefix, with sum 0, that starts the map
Start: nums = [1, 2, 3, -3, 3] and k = 3. The map must know the empty prefix before the loop starts.
Gotcha 1 of 4
- The trap
Gotcha 1: Leaving out
prefix_counts[0] = 1Mistakeprefix_counts = defaultdict(int)Fixprefix_counts = defaultdict(int) prefix_counts[0] = 1What happens: A subarray that starts at index 0 has the sum
running_sum. So itsneeded_prefixis 0. Prefix sum 0 belongs to the empty prefix, before any element. Withoutprefix_counts[0] = 1, that lookup finds nothing, and those subarrays are not counted.Example: nums = [1, 1, 1], k = 2Mistake: returns 1Fix: returns 2
- All values are positive and you want the shortest subarray whose sum is at least a target, as in Minimum Size Subarray Sum (LC 209), not a count of exact sums. See Shortest Window
- The input is a list of updates that each add to a whole range, and you need every final value, as in Range Addition (LC 370), not subarrays with a given sum. See Difference Array
Deep Dive
From a search over ranges to a lookup
The Prefix Sums card answers a question about a range you already know. Here the ranges are unknown. You must count the ranges nums[L..R] whose sum is k.
The range sum is P[R + 1] - P[L]. It equals k exactly when P[L] = P[R + 1] - k. So at each element, the question is how many earlier prefixes have the value running_sum - k. That value is needed_prefix.
A hash map answers the question in constant time. prefix_counts[v] is the number of earlier prefixes with the value v. The loop adds prefix_counts[needed_prefix] to total_matches, and it counts each subarray once, at its last element.
The map keeps a count and not a yes or no, because several earlier prefixes can have the same value. Each of them is the start of a different subarray.
Nothing in this argument uses the sign of a value. It works with negative numbers. A shrinking window does not: for nums = [1, -1, 1] and k = 1, a window that shrinks when the sum is above k finds 2 subarrays, but the answer is 3.
The loop traced on [1, 2, 3, -3, 3] with k = 3
The prefix sums are 0, 1, 3, 6, 3, 6. The map starts as {0: 1}, because the empty prefix occurs once.
At nums[1], running_sum = 3 and needed_prefix = 0. The map holds 0 once. That match is the subarray [1, 2], which starts at index 0.
At nums[2], running_sum = 6 and needed_prefix = 3. The prefix 3 occurred once, so the subarray [3] is counted.
At nums[3] = -3, running_sum returns to 3 and needed_prefix = 0. The subarray [1, 2, 3, -3] is counted. Then prefix_counts[3] becomes 2.
At nums[4], running_sum = 6 and needed_prefix = 3. The map holds 3 twice, so two subarrays are counted: [3, -3, 3] and [3]. The total is 5.
The trap is the first line of the code. Without prefix_counts[0] = 1, the matches that use the empty prefix are missed. Here that loses [1, 2] and [1, 2, 3, -3], and the answer would be 3.
Order of the lookup and the record
The loop looks up needed_prefix first and records running_sum after. The lookup must see only earlier prefixes.
If the record comes first and k = 0, then needed_prefix equals running_sum. The lookup finds the current prefix itself and counts an empty subarray. For nums = [1] and k = 0, the answer would be 1 instead of 0.
Changing the key for other questions
The template takes a key and a needed_key. Only these two change between problems. The loop stays the same.
For a sum divisible by k, as in Subarray Sums Divisible by K (LC 974), two prefixes with the same remainder P % k enclose a sum divisible by k. The key is the remainder. For nums = [4, 5, 0, -2, -3, 1] and k = 5, the answer is 7.
For the longest subarray, as in Maximum Size Subarray Sum Equals k (LC 325), store the first index of each prefix and not a count. The longest subarray that ends at i starts right after the first index of running_sum - k. For nums = [1, -1, 5, -2, 3] and k = 3, the answer is 4.
For equal numbers of 0 and 1, as in Contiguous Array (LC 525), replace each 0 by -1. A subarray with equal numbers now has the sum 0. For [0, 1, 0], the answer is 2.
The cost of every version is O(N) time. The map holds up to N + 1 prefixes, so the extra space is O(N).
60-Second Pitch
"I need the number of subarrays with sum k, and values can be negative, so a window will not work. A subarray from L to R has the sum P[R + 1] - P[L]. It equals k when P[L] equals running_sum - k. So at each element I look up how many earlier prefixes have that value. I keep the counts in a hash map and start with prefix_counts[0] = 1 for the empty prefix. I look up first and record after, so a subarray never matches itself. The time is O(N) and the space is O(N). For divisibility the key becomes the remainder, and for the longest subarray the map stores the first index."
Common Questions
- Why does a sliding window fail here, and this method does not?
A window moves by a rule such as "shrink while the sum is too big". That rule needs the sum to grow when the window grows, which fails with negative values. The map method only uses the identity
P[R + 1] - P[L] = k, which holds for any values.- Why does the map store counts and not just which prefixes exist?
Several earlier prefixes can have the same value. Each one starts a different subarray that ends at the current element. The count says how many.
- Why is
prefix_counts[0] = 1needed before the loop? A subarray that starts at index 0 has the sum
running_sum, so its needed prefix is 0. The prefix 0 is the empty prefix, before any element. The map must hold it, or those subarrays are missed.- How do you find the longest subarray and not the count?
Store the first index where each prefix sum appears, with
0at index-1. At indexi, ifrunning_sum - kis in the map, the length isiminus that first index. Never overwrite a stored index, because the first index gives the longest subarray.- What is the cost, and what does the map cost in space?
The time is
O(N)with an average constant cost per map operation. The map holds up toN + 1different prefixes, so the extra space isO(N). The Prefix Sums card needs the same space but answers only known ranges.
SWE Reasoning
Spot it
- The question counts or finds subarrays with an exact sum, or with a sum divisible by
k, or with equal numbers of two kinds of values. - Values can be negative, so a window that grows and shrinks cannot decide which way to move.
- The ranges are unknown. The target is known.
- Example problems: Subarray Sum Equals K (LC 560), Subarray Sums Divisible by K (LC 974), Contiguous Array (LC 525).
The invariant, step by step
- Before the loop,
prefix_counts[0] = 1: the empty prefix occurs once. - For each
num,running_sumbecomes the prefix sum that ends atnum. prefix_countsholds the earlier prefixes only, so the lookup ofneeded_prefix = running_sum - kcounts the subarrays that end atnumand sum tok.- Only then is
running_sumadded toprefix_counts, so a subarray never matches itself. - Every subarray ends at exactly one element, so
total_matchescounts each subarray once.
Traps
- Leaving out
prefix_counts[0] = 1. Every subarray that starts at index 0 is missed. For[1, 1, 1]andk = 2, the answer is 1 instead of 2. - Recording before the lookup. With
k = 0the lookup finds the current prefix and counts an empty subarray. - Using a set and not a count. Two earlier prefixes with the same value are two different subarrays, and a set counts them once.
- Keeping the last index for the longest subarray. The first index gives the longest one. A later index overwrites it and makes the answer shorter.
- Using the sum as the key for divisibility. The key is
running_sum % k.