DSA sheet · Arrays · Prefix Sum pattern

Subarray Sum Equals K

This is the first question of the Prefix Sum pattern, and the teacher spends two videos on it. In Part 1 she writes the brute force (two loops), reads the constraints, rules out the other array patterns one by one, and then writes the prefix sum + hashmap solution. In Part 2 she comes back only to build the intuition slowly: why a plain prefix sum array is not enough, why we need a map of counts, and why the map starts with {0: 1}.

Why it matters: "count subarrays whose sum is exactly k" is a pattern you will see again and again (count subarrays divisible by k, binary subarrays with sum, nice subarrays…). The same trick of "remember how many times each prefix sum has appeared" solves all of them.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a subarray?

A subarray is a piece of the array where the elements sit next to each other (contiguous). You choose a start index l and an end index r (with l ≤ r) and take everything between them. You can't skip elements in the middle.

index0123
nums4719[7, 1] is a subarray (l=1, r=2)
nums4719[4, 1] is not a subarray (7 is skipped)

An array of length n has n·(n+1)/2 non-empty subarrays. For n = 4 that's 10.

What is a prefix sum?

A prefix is the start of the array: everything from index 0 up to some index i. The prefix sum at i is the total of that start part:

prefix[i] = nums[0] + nums[1] + … + nums[i]

We don't add everything again for every i. Each cell is just the cell before it plus one new number:

prefix[0] = nums[0]  and  prefix[i] = prefix[i-1] + nums[i]

The teacher builds it on the same array she uses for the whole problem:

index0123456
nums1-1012-13
prefix1001325
ihow prefix[i] is builtprefix[i]
0just nums[0] = 11
1prefix[0] + nums[1] = 1 + (−1)0
2prefix[1] + nums[2] = 0 + 00
3prefix[2] + nums[3] = 0 + 11
4prefix[3] + nums[4] = 1 + 23
5prefix[4] + nums[5] = 3 + (−1)2
6prefix[5] + nums[6] = 2 + 35

Why sum(l..r) = prefix[r] − prefix[l−1]

Both prefixes start at index 0. The longer one, prefix[r], covers 0…r. The shorter one, prefix[l−1], covers 0…l−1. If you take the long one and remove the short one, only the piece l…r is left. The teacher says it as "bigger one minus smaller one".

  prefix[r]    = |  0 ........ l-1  |  l ........ r  |
  prefix[l-1]  = |  0 ........ l-1  |
  difference   =                    |  l ........ r  |   = sum(l..r)

Her example: what is the sum of nums[1..3] = (−1, 0, 1)?

index0123456
nums1-1012-13we want the yellow part
prefix1001325prefix[3] − prefix[0] = 1 − 1 = 0

prefix[3] is 1 − 1 + 0 + 1 = 1. prefix[0] is 1. The difference is 0, and indeed −1 + 0 + 1 = 0 ✓. One subtraction instead of a loop.

The "−1" problem and the empty prefix = 0 trick

What if l = 0? Then we need prefix[−1], which doesn't exist. But think about what it means: "the sum of the elements before index 0". There are none, so it's the sum of an empty prefix, which is 0. So:

Keep this in your headThe empty prefix (sum 0, before index 0) is a real prefix. Forgetting it is exactly the bug that map[0] = 1 fixes in Part B.
prefix sum + range query (Part 0 helper)
def build_prefix(nums):
    P = [0] * (len(nums) + 1)          # P[0] = 0 is the empty prefix
    for i in range(len(nums)):
        P[i + 1] = P[i] + nums[i]      # previous total + one new number
    return P

def range_sum(P, l, r):
    return P[r + 1] - P[l]             # (0..r) minus (0..l-1)

What a prefix sum array alone is good for, and what it is NOT good for

The teacher makes this point in Part 2. A prefix array answers "what is the sum from l to r?" in O(1), and that's great when the question gives you many (l, r) queries, or asks a yes/no question. But here the question is "how many subarrays have sum k?" We don't know l and r; we'd have to try every pair, which brings back O(n²). For counting, she adds a frequency map.

What is a hashmap (dict) of counts?

A Python dict stores key → value pairs, and looking up a key takes O(1) on average. A frequency map (or count map) uses the thing we have seen as the key and how many times we've seen it as the value.

a frequency map in 3 lines
freq = {}
for x in [5, 2, 5, 5]:
    freq[x] = freq.get(x, 0) + 1     # {5: 3, 2: 1}

freq.get(x, 0) means "the count of x, or 0 if x isn't there yet". It's the same as Java's getOrDefault, which the teacher uses.

The 4 array patterns the teacher always checks

Whenever she wants to optimise an array problem, she goes through the four patterns from the first video of the playlist and asks which one fits:

patternwhen it works
Two pointersOnly the two ends matter (the elements in between don't), usually on a sorted array.
Sliding windowA contiguous range (window) where all numbers are positive, so growing the window always increases the sum and shrinking always decreases it.
Kadane's algorithmContiguous sums with negatives allowed, but only to find the maximum sum.
Prefix sumSums over contiguous ranges, negatives allowed, many range queries. Add a hashmap when you must count subarrays with an exact sum.

Part A · Brute force: try every subarray

LeetCode 560 · from video 1

1The question in simple words

You get an array of integers nums and an integer k. Return how many subarrays have a sum of exactly k.

Two things to notice. First, it asks for a count, not True/False and not the subarrays themselves. Second, it says exactly k, not "at most k". Both details decide which pattern we can use later.

index0123456
nums1-1012-13k = 3 → answer 4

The 4 subarrays with sum 3 are:

LeetCode's small examples: [1,1,1], k=2 → 2 and [1,2,3], k=3 → 2 ([1,2] and [3]).

2What the constraints tell us

3Intuition: list every subarray and add it up

The simplest idea: if we could go through every subarray, add up its numbers and check whether the total is k, we just do count += 1 each time it is.

Picture it as two fingers. The left finger marks where the subarray starts. The right finger starts at the same place and walks to the end, one step at a time. Each position of the right finger is one subarray l..r. When the right finger reaches the end, the left finger moves one step and the right finger starts again from there.

4Building the logic from the example

Start the left finger at index 0

We keep a running sum for the current start. Each time the right finger moves, we add the one new number. We don't re-add the whole subarray.

rnums[r]subarray 0..rrunning sum= 3?
01[1]1no
1−1[1,−1]0no
20[1,−1,0]0no
31[1,−1,0,1]1no
42[1,−1,0,1,2]3yes → count = 1
5−1[…,−1]2no
63[…,3]5no

Starting at index 0 we found exactly one valid subarray.

Doubt 1: we found sum 3 at r = 4. Why don't we stop there?
→ Because the numbers can be negative (and zero). The sum went 3 → 2 → 5 here, but with other numbers it could come back down to 3 later. With negatives you can never say "it's past k, so nothing further can work". We must walk the right finger all the way to the end.

Move the left finger to index 1

The running sum must start from 0 again, because this is a new subarray family. Right finger from 1: −1, −1, 0, 2, 1, 4. None of them is 3 → no valid subarray starting at index 1.

Doubt 2: where exactly do I reset the sum?
→ Inside the outer loop, just before the inner loop starts (s = 0). If you reset it outside both loops, the sum from the previous start leaks into the new one.

And so on for every left position

Doing this for every start gives one subarray from start 0, one from start 2 (0,1,2), one from start 3 (1,2) and one from start 6 (just 3). Total 4. Notice: we never printed the subarrays. The question only wants the count, so a counter is enough.

5Approach steps

  1. count = 0.
  2. For each start l from 0 to n−1: set s = 0.
  3. For each end r from l to n−1: add nums[r] to s.
  4. If s == k, do count += 1.
  5. After both loops, return count.

6Code (Python)

Brute force: two loops, O(n²)
class Solution:
    def subarraySum(self, nums, k):
        n = len(nums)
        count = 0
        for l in range(n):               # where the subarray starts
            s = 0                        # fresh sum for this start
            for r in range(l, n):        # where it ends
                s += nums[r]             # sum of nums[l..r]
                if s == k:
                    count += 1
        return count

7Code line by line

linewhat it means
count = 0The answer: how many subarrays had sum k.
for l in range(n):The left finger. Every index gets a turn as the start.
s = 0Reset the sum for the new start.
for r in range(l, n):The right finger starts at l (so a single element counts as a subarray) and goes to the end.
s += nums[r]Extend the subarray by one element. Now s = sum(l..r).
if s == k: count += 1This subarray is valid, so count it. Don't break, since a later r might also work.
return countThe total after checking all n·(n+1)/2 subarrays.

8Dry run (hand table)

nums = [1, −1, 0, 1, 2, −1, 3], k = 3. Each row is one start l. The running sums are listed in order as r moves right; ✓ marks a hit.

lrunning sums as r goes l → 6hitscount after
01, 0, 0, 1, 3 ✓, 2, 50..41
1−1, −1, 0, 2, 1, 4none1
20, 1, 3 ✓, 2, 52..42
31, 3 ✓, 2, 53..43
42, 1, 4none3
5−1, 2none3
63 ✓6..64

Final answer: 4 ✓, the same four subarrays we listed in step 1.

9Complexity & remember

The teacher submitted it and it was accepted but very slow. That is her signal to look for a better pattern.

Doubt: will it pass in Python too?
→ Her submission was in Java. Python is much slower per step, so ~2·10⁸ steps will most likely give TLE in Python. One more reason to learn Part B.
Remember the brute forceLeft finger = start, right finger = end. Reset the sum for each start, add one number per step, count every time it equals k, and never break early (negatives!).

Part B · Optimal: prefix sum + hashmap of counts

LeetCode 560 · code from video 1, intuition from video 2

1The question again, with the new goal

Same question: count the subarrays with sum exactly k. The new goal is to do it in one pass (one loop, O(n)) instead of two nested loops.

2What the constraints tell us now

3Choosing the pattern: why the other three fail

The teacher goes through her four patterns and rules out three of them, giving a reason for each. This reasoning is what interviewers want to hear.

✗ Two pointers

Two pointers is for questions where only the two end points matter and nothing in between (and, as she adds in video 2, usually when the array is sorted). Here we need the sum of everything between l and r, and the array isn't sorted. So two pointers is out.

✗ Sliding window

Sliding window also works on contiguous sums, but it has one rule: it works only when the numbers are positive. Once a negative number can be inside the window, it breaks. Here's why. Sliding window decides with two moves:

With negatives, both promises are false: adding a negative makes the sum smaller, and removing a negative makes it bigger. The window moves the wrong way and skips answers. A small example (my own, to show her point):

index012
nums2-12k = 2, correct answer = 2 ([2] at 0 and [2] at 2)
  1. Window [2] → sum 2 = k → count 1.
  2. Grow: [2, −1] → sum 1, too small → grow.
  3. Grow: [2, −1, 2] → sum 3, too big → shrink from the left: [−1, 2] → sum 1, now too small → stop shrinking.
  4. The right end is at the last index → done. Count = 1. Wrong. The window never looked at [2] alone at index 2, because it stopped shrinking when the sum dropped to 1. It had no way to know that removing the −1 would push the sum back up to 2.

✗ Kadane's algorithm

Kadane's does handle negatives, but it answers a different question: the maximum subarray sum. It keeps one best value. It can't tell you how many subarrays hit an exact total. So it's out too.

✗ The "right − left + 1" counting trick

In earlier sliding-window problems, the teacher counted subarrays by adding right − left + 1 for each window. She explains why that doesn't work here: that trick counts subarrays with sum at most k (≤ k), and only for positive numbers. Here we need exactly k, and there are negatives.

Her rule of thumbContiguous sum + negatives allowed + count subarrays with sum exactly k → prefix sum + hashmap. The hashmap is what does the counting, because it stores the frequency of each prefix sum.

4Building the idea from examples (the intuition from video 2)

Step 1: a single running sum is a prefix sum

Walk through the array once with one pointer and keep a running total s. At index i, s is the sum of everything from 0 to i, so s is prefix[i]. We don't need to store a prefix array. The variable s holds the current prefix, and the map will remember the old ones.

On our array, s goes: 1, 0, 0, 1, 3, 2, 5. At index 4, s = 3 = k. That tells us subarray 0..4 is valid. But how many valid subarrays end at index 4? A single "s == k?" check only finds subarrays that start at 0. We'd miss 2..4 and 3..4.

Step 2: the "leftover" picture

Fix the end at index i. Any subarray that ends at i looks like this: the whole prefix (0..i) is split into a front part (0..j−1) and the subarray (j..i):

  0 ............ j-1 | j ............ i
 |----- front -------|---- subarray ---|
 |------------ s = prefix[i] ----------|

  subarray sum = s - (front sum)
  we want it = k   →   front sum must be  s - k

So asking "which subarrays ending at i sum to k?" is the same as asking "how many earlier prefixes had sum exactly s − k?" Each such earlier prefix is a different place to cut, and each cut leaves a different subarray with sum k.

The teacher calls s − k the remaining or leftover part. She stores it in a variable named target: target = prefixSum − k.

Step 3: why the map stores COUNTS, not just "seen / not seen"

On our array at index 4, s = 3, so target = 3 − 3 = 0. Which earlier prefixes had sum 0? Let's list them:

index·01234
nums·1-1012we stand at index 4
prefix010013the empty prefix + index 1 + index 2

Prefix sum 0 has appeared 3 times (counting the empty prefix before index 0). Each one gives a cut:

front part (sum 0)leftover subarray (sum 3)
empty (nothing)[1, −1, 0, 1, 2] (index 0..4)
[1, −1][0, 1, 2] (index 2..4)
[1, −1, 0][1, 2] (index 3..4)

Three cuts → three subarrays ending at index 4. That's why a set ("have I seen 0?") is not enough. We need how many times each prefix sum appeared. The teacher's way to say it: however many times the front part (sum 0) has occurred, the leftover part (sum 3) has occurred the same number of times, because together they always make the full sum 3.

Doubt 1: can the same prefix sum really appear many times?
→ Yes, because of zeros and negatives. In our array, the sum climbs to 1 and comes back to 0 (after −1), then stays 0 (after the 0). With only positive numbers the prefix sums would strictly increase and never repeat. That's another reason sliding window is enough for positives but not here.

Step 4: why map[0] = 1 at the start (her "aha" moment in video 1)

In video 1, the teacher first fills the map without anything in it at the start. Watch what happens at index 4:

ismap without the starting entry
01{1:1}
10{1:1, 0:1}
20{1:1, 0:2}
31{1:2, 0:2}
43look up 3 − 3 = 0 → found 2

It finds only 2 subarrays ending at index 4 (2..4 and 3..4), but there are 3. The missing one is the whole prefix 0..4, the subarray that starts at index 0. Its front part is the empty prefix, whose sum is 0, and we never recorded it.

An even simpler case she gives: nums = [3], k = 3. s = 3, target = 0, and the map is empty → answer 0. Wrong, since [3] itself is valid.

The fix: put the empty prefix in the map before the loop: freq = {0: 1}. It means "a prefix with sum 0 has been seen once: the empty one, before index 0". Now at index 4 the count of 0 is 3, and for [3] the lookup of 0 finds 1 ✓.

Doubt 2: the empty subarray isn't allowed as an answer. Aren't we counting it?
→ No. The empty prefix is only ever used as the front part (the bit we cut away). The subarray we count is the leftover, which is everything from index 0 to i, and that's never empty. {0: 1} just lets "subarrays starting at index 0" be counted, the same "empty prefix = 0" idea as prefix[−1] = 0 in Part 0.

Step 5: look up first, then add the current prefix

At each index we do two things, in this order:

  1. Look up s − k and add its count to the answer.
  2. Record the current s in the map: freq[s] = freq.get(s, 0) + 1.
Doubt 3: in her pseudo-code she said "if the target is found, add the count; if not, put the sum in the map". Is the put only for the "not found" case?
→ No, and she corrects this herself in video 2 (she says the put should have been written for the other case too). The current prefix sum must be recorded every time, whether or not the target was found, because a later index might need it as its front part. In the code, the put sits outside the if.
Doubt 4: why must the lookup happen before the put?
→ The front part must end before the subarray starts, so only earlier prefixes may be used. If you put s first and then look up s − k, then when k = 0 you'd find the prefix you just added. That's the "front = everything, subarray = empty" cut, and it would count an empty subarray at every index. Example: nums = [1], k = 0: the right answer is 0, but put-first gives 1. The teacher's order (look up, then put) avoids this. My addition: the k = 0 case isn't in her video, but the constraints allow it, so it's tested below.
Doubt 5: on screen she once wrote map.put(nums[r], …). Is the key the number or the sum?
→ The prefix sum. She noticed the slip and corrected it to prefixSum. The map is about totals so far, not single elements.

5Approach steps

  1. freq = {0: 1}: the empty prefix has been seen once.
  2. s = 0 (running prefix sum), count = 0.
  3. For each number x in nums: s += x.
  4. target = s − k. If target is in freq, add freq[target] to count. That's the number of subarrays ending here with sum k.
  5. Record the current prefix: freq[s] = freq.get(s, 0) + 1 (always).
  6. After the loop, return count.

6Code (Python)

Optimal: prefix sum + hashmap, O(n)
class Solution:
    def subarraySum(self, nums, k):
        freq = {0: 1}          # empty prefix: sum 0 seen once
        prefix_sum = 0
        count = 0
        for x in nums:                     # one pass, like her for-each loop
            prefix_sum += x                # sum of nums[0..i]
            target = prefix_sum - k        # the front part we need
            if target in freq:
                count += freq[target]      # one subarray per earlier cut
            freq[prefix_sum] = freq.get(prefix_sum, 0) + 1   # always record
        return count

Shorter way, same meaning: replace the if with count += freq.get(prefix_sum - k, 0). Some people use collections.defaultdict(int) for freq. Both work the same.

7Code line by line

linewhat it means
freq = {0: 1}Key = a prefix sum, value = how many times it has appeared. The empty prefix (sum 0) counts once, so subarrays that start at index 0 are found.
prefix_sum = 0The running total. After adding nums[i], it equals prefix[i].
count = 0The answer.
for x in nums:One pointer, one pass. The teacher says a normal index loop or a for-each loop both work.
prefix_sum += xExtend the prefix by one element.
target = prefix_sum - kIf an earlier prefix had this sum, the part after it (up to here) sums to k.
if target in freq: count += freq[target]Every earlier prefix with that sum is a separate starting cut, so add all of them, not just 1.
freq[prefix_sum] = freq.get(prefix_sum, 0) + 1Record the current prefix so later indexes can use it as a front part. If it's new, it starts at 0 + 1 = 1. If it's seen before, increase it. Done after the lookup, every time.
return countThe total number of subarrays with sum exactly k.

8Dry run (hand table, the map grows step by step)

nums = [1, −1, 0, 1, 2, −1, 3], k = 3. Start: freq = {0:1}, prefix_sum = 0, count = 0.

inums[i]running sumlook up (sum − k)foundcountmap after this step
011−200{0:1, 1:1}
1−10−300{0:2, 1:1}
200−300{0:3, 1:1}
311−200{0:3, 1:2}
423033{0:3, 1:2, 3:1}
5−12−103{0:3, 1:2, 3:1, 2:1}
635214{0:3, 1:2, 3:1, 2:1, 5:1}

The same steps as a story, the way she narrates it:

  1. i=0: sum 1. Need −2? Not there. Record 1 → {0:1, 1:1}.
  2. i=1: sum 0. Need −3? No. 0 is already in the map, so its count goes to 2.
  3. i=2: sum still 0. Need −3? No. 0 goes up to 3. "Zero has now appeared three times."
  4. i=3: sum 1. Need −2? No. 1 goes up to 2.
  5. i=4: sum 3. Don't think "sum is 3, so that's one answer". Look up 3 − 3 = 0 → it appeared 3 times → three subarrays end here: [1,−1,0,1,2], [0,1,2], [1,2]. count = 3. Record 3.
  6. i=5: sum 2. Need −1? No. Record 2.
  7. i=6: sum 5. Need 5 − 3 = 2 → it appeared once (the prefix up to index 5). The front part 1,−1,0,1,2,−1 sums to 2, so the leftover [3] sums to 3. count = 4. Record 5.
  8. Loop ends → return 4 ✓, the same answer as the brute force.
index0123456
nums1-1012-13
prefix10013255 − 2 = 3 → the subarray [3] at index 6

9Complexity & remember

She submitted it and it beat about 94% of solutions, far faster than the brute force. A small trade: we spend O(n) memory to save a whole loop.

Remember the optimal freq = {0:1} → for each x: s += x → count += freq[s − k] (if present) → freq[s] += 1.
"Earlier prefix with sum s − k" = "a subarray ending here with sum k". Count them all.

Part C · Revision page

Brute forcePrefix sum + hashmap
ideatry every (l, r), add as you gofor each end, count earlier prefixes equal to s − k
loopstwo nestedone
extra memorynonedict: prefix sum → how many times
handles negatives / k ≤ 0yesyes
time / spaceO(n²) / O(1)O(n) / O(n)
on LeetCodeaccepted but slow (Java), likely TLE in Pythonfast (~94%)
patternwhy it does / doesn't fit this problem
Two pointersonly the ends matter, needs sorted data. We need everything in between.
Sliding windowneeds positive numbers. Negatives make grow/shrink go the wrong way.
right − left + 1 trickcounts "sum ≤ k", not "exactly k".
Kadane'sfinds the max sum, can't count.
Prefix sum + hashmapcontiguous sums, negatives OK, counts exact matches.
If you remember only 5 lines 1. sum(j..i) = prefix[i] − prefix[j−1], and the empty prefix is 0.
2. A subarray ending at i has sum k ⇔ some earlier prefix equals s − k.
3. Store counts of prefix sums: each earlier occurrence is a different start.
4. Start with {0: 1} so subarrays starting at index 0 are counted.
5. Look up first, then record the current sum. Record it every time.
Mistakes to avoid ✗ forgetting {0: 1} (misses subarrays starting at index 0)
✗ count += 1 instead of count += freq[s − k]
✗ using a set instead of counts
✗ putting the current sum before the lookup (wrong when k = 0)
✗ putting the sum only when the target isn't found
✗ using nums[i] as the map key instead of the prefix sum
✗ reaching for sliding window when the array has negatives
test it yourself (paste under either Solution above)
s = Solution()
print(s.subarraySum([1, -1, 0, 1, 2, -1, 3], 3))   # 4
print(s.subarraySum([1, 1, 1], 2))                 # 2
print(s.subarraySum([1, 2, 3], 3))                 # 2
print(s.subarraySum([3], 3))                       # 1  (needs {0: 1})
print(s.subarraySum([0, 0, 0], 0))                 # 6
print(s.subarraySum([2, -1, 2], 2))                # 2  (sliding window says 1)

Based on these videos: Subarray Sum Equals K · Part 1 (brute force → prefix sum) · Subarray Sum Equals K · Part 2 (prefix sum + hashmap intuition)