The sliding window dies the moment a negative number appears. This is what replaces it: precompute every running total, then let a hash map find the pair you need in one pass.
Pattern 1 ends with a warning. A window only works while the running total grows as the window widens, so one negative value breaks it. Prefix sums have no such requirement. They turn “the sum of a range” into a single subtraction, and a hash map turns “find the range I want” into a single lookup.
| Signal | What it looks like |
|---|---|
| Count, not find | “how many subarrays sum to k”. A window finds one; a prefix map counts all. |
| Exact equality | “sum equals k”, not “sum at least k”. Equality is not monotone, so shrinking is unsafe. |
| Negative values allowed | The constraint list says values may be negative. That single line rules out the window. |
| Divisibility | “sum is a multiple of k”. Work with remainders instead of sums. |
| Many range queries | “answer q queries for the sum of a[i..j]”. Build the prefix array once, then each query is O(1). |
| Everything except me | “product of all other elements”, “sum of everything but this”. Prefix and suffix passes, no map needed. |
| Sliding window | Prefix sum plus map | |
|---|---|---|
| Needs | Non-negative values, monotone condition | Nothing. Any values. |
| Answers | Longest, shortest, best | Count, exists, exact match |
| Space | O(1) | O(n) for the map |
| Typical prompt | “smallest subarray with sum at least k” | “how many subarrays sum to exactly k” |
P[0] = 0 and P[i] = nums[0] + … + nums[i-1]. Then the sum of any range is a single subtraction: sum(nums[i..j]) = P[j+1] - P[i]. A range query that cost O(n) now costs O(1), after one O(n) build.The subtraction alone still leaves O(n²) pairs to check. The second half of the pattern removes that.
P[j+1] - P[i] == k. Rearrange it: P[i] == P[j+1] - k. So walk the array once keeping a running P, and at each step ask the hash map how many earlier prefixes had the value P - k. Each answer is a valid subarray ending here. One pass, O(n).This is the same rearrangement that turns Two Sum from O(n²) into O(n). It is worth saying so out loud, because it shows you see the shared idea rather than two memorised tricks.
{0: 1}. That entry stands for the empty prefix, the sum before any element. Without it you miss every subarray that starts at index 0. This one line is the most common bug in the pattern, and it is invisible until a test happens to need it.from itertools import accumulate
def build_prefix(nums: list[int]) -> list[int]:
"""P[i] is the sum of the first i elements. len(P) == len(nums) + 1."""
# Leading 0: P[0] is the sum of zero elements. accumulate adds the
# running totals after it, so P[i] covers nums[0..i-1].
return [0, *accumulate(nums)]
def range_sum(prefix: list[int], i: int, j: int) -> int:
"""Sum of nums[i..j] inclusive, in O(1)."""
# prefix[j + 1] sums nums[0..j]. +1 because P is shifted by the leading 0.
# Subtract prefix[i], the sum of nums[0..i-1], to leave nums[i..j].
return prefix[j + 1] - prefix[i]
itertools.accumulate does the build in one C-level pass. Know the manual loop too, but this is what you would ship.
from collections import defaultdict
def count_ranges(nums: list[int], k: int) -> int:
"""Count subarrays whose sum equals k."""
# prefix value -> how many earlier prefixes had it. Missing keys read as 0.
seen: defaultdict[int, int] = defaultdict(int)
# seen[0] = 1: the empty prefix (sum 0) is seen once before the array starts.
# Without it, ranges from index 0 are lost.
seen[0] = 1
running = 0 # 0: sum of no elements yet
count = 0 # 0: no ranges found yet
# Invariant: running is the sum of nums up to and including value.
# seen holds every prefix that ends BEFORE value.
for value in nums:
running += value
# A range ends here with sum k exactly when an earlier prefix
# equals running - k. Look up BEFORE recording this prefix,
# so the empty range is never counted.
count += seen[running - k]
seen[running] += 1 # +1: this prefix is now seen one more time
return count
Look up before you record. Recording first would let a prefix pair with itself, which is an empty subarray.
Reading the figure. The top row is nums. The bottom row is the running prefix P. Each arc goes from the current prefix back to an earlier one that is exactly k smaller. Violet is the match found at P = 3. Green is the two matches found at P = 5. The value 2 shows up twice in P, so the map must store a count, not a flag.
def combine_around_each(nums: list[int]) -> list[int]:
"""For each index, combine everything to its left with everything to its right."""
# combine: the operation that merges values. Here it is +.
# Product of Array Except Self uses * with a start value of 1.
n = len(nums)
answer = [0] * n # 0: the identity for +, so it adds nothing yet
# running starts at 0, the identity for +. It means "nothing to the left".
running = 0
# Left to right. i is the current index.
# Invariant: running holds the combine of nums[0..i-1], strictly left of i.
for i in range(n):
answer[i] = running # store the left part before nums[i] joins it
running += nums[i] # now running covers nums[0..i]
running = 0 # reset to 0: nothing to the right yet
# Right to left: start at n - 1 (last index), stop after 0.
# -1 as the stop so index 0 is included. -1 as the step walks backwards.
# Invariant: running holds the combine of nums[i+1..n-1], strictly right of i.
for i in range(n - 1, -1, -1):
answer[i] += running # merge the right part into the left part
running += nums[i] # now running covers nums[i..n-1]
return answer
Two sweeps in opposite directions, with the answer array doubling as the scratch space. This is how you get O(1) extra space on “everything except me” problems.
Reading the figure. Each column is one index. The amber cell is the index in focus. Pass 1 walks right and stores the total of everything to the left (blue). Pass 2 walks left and holds the total of everything to the right (violet). The green cell adds the two. Read any column top to bottom to get its answer.
{0: 1} seed. Every subarray starting at index 0 vanishes.k is 0.P has n + 1 entries, and sum(i..j) is P[j+1] - P[i]. Write that line down before coding.dict and indexing a missing key. Use defaultdict(int) or .get(key, 0).Given an array of integers and a target k, return the number of contiguous subarrays whose sum equals k. Values may be negative.
k is the number of earlier prefixes equal to running - k.from collections import defaultdict
def subarray_sum(nums: list[int], k: int) -> int:
"""Count contiguous subarrays of nums summing to exactly k.
Args:
nums: Integers. Negative values are allowed.
k: The exact target sum.
Returns:
The number of qualifying subarrays.
Example:
>>> subarray_sum([1, 2, 3], 3)
2
"""
# prefix value -> how many times it has been seen so far.
# defaultdict(int): a missing key reads as 0, meaning "never seen".
seen: defaultdict[int, int] = defaultdict(int)
# The empty prefix has sum 0 and is seen 1 time before any element.
# It lets a subarray that starts at index 0 be counted.
seen[0] = 1
running = 0 # 0: sum of no elements yet
count = 0 # 0: no subarrays found yet
# Invariant: running = sum of nums up to and including value.
# seen holds all prefixes that end before value.
for value in nums:
running += value
# Every earlier prefix equal to (running - k) marks the start of a
# subarray ending here whose sum is exactly k.
count += seen[running - k]
# Record this prefix AFTER the lookup. +1: seen once more.
seen[running] += 1
return count
nums = [1, 2, 3], k = 3:
| value | running | running - k | seen[running-k] | count | seen after |
|---|---|---|---|---|---|
| 1 | 1 | -2 | 0 | 0 | {0:1, 1:1} |
| 2 | 3 | 0 | 1 | 1 | {0:1, 1:1, 3:1} |
| 3 | 6 | 3 | 1 | 2 | {0:1, 1:1, 3:1, 6:1} |
The two subarrays are [1, 2] and [3]. Notice the seed {0: 1} is what found [1, 2].
0:1 finds [1, 2], and the stored 3:1 finds [3].Reading the figure. Each box is one turn of the loop. The first lines show the lookup for running - k. The row of small cells is the seen map after the step. Green marks the key the lookup found. Amber marks the new prefix just recorded. Notice the lookup always happens before the amber key is added.
def longest_subarray_sum(nums: list[int], k: int) -> int:
"""Length of the LONGEST subarray summing to k. Store first index, not counts."""
# prefix sum -> first index where it appeared.
# {0: -1}: the empty prefix (sum 0) "ends" at index -1, before the array.
# So a match at index i gives length i - (-1) = i + 1, the whole prefix.
first_index: dict[int, int] = {0: -1}
running = 0 # 0: sum of no elements yet
best = 0 # 0: no qualifying subarray found yet
# index is the current end. Invariant: running = sum of nums[0..index].
for index, value in enumerate(nums):
running += value
# An earlier prefix of running - k means nums[start+1..index] sums to k.
if running - k in first_index:
# Length = index - start. No +1: start is the index BEFORE the range.
best = max(best, index - first_index[running - k])
# Only record the FIRST time a prefix appears: earlier start, longer span.
if running not in first_index:
first_index[running] = index
return best
k == 0 with zeros in the array, such as [0, 0, 0]: the answer is 6. Good test of the look-up-then-record order.k: works unchanged.0.0.P[j] - P[i] == k into P[i] == P[j] - k, which is a hash lookup, exactly like Two Sum.”Return True if the array has a contiguous subarray of length at least two whose sum is a multiple of k. k is a positive integer, and a multiple includes 0 × k.
P[j] - P[i] is a multiple of k exactly when P[j] % k == P[i] % k. So stop storing sums and store remainders. The question becomes: have I seen this remainder before, far enough back? Two equal remainders bracket a valid subarray.There are only k possible remainders, so on a long array a repeat is guaranteed. The whole difficulty is the length-at-least-two rule.
{0: -1}. The empty prefix has remainder 0 and sits before index 0, which makes the length arithmetic work for a subarray starting at the beginning.first_index[remainder] + 1 to index, so its length is index - first_index[remainder]. Require at least 2.def check_subarray_sum(nums: list[int], k: int) -> bool:
"""True if some subarray of length >= 2 sums to a multiple of k.
Args:
nums: Non-negative integers.
k: A positive divisor.
Returns:
True if such a subarray exists.
Example:
>>> check_subarray_sum([23, 2, 4, 6, 7], 6)
True
"""
# remainder -> earliest index where the running sum had it.
# {0: -1}: the empty prefix has remainder 0 and ends at index -1.
# It lets a range that starts at index 0 qualify.
first_index: dict[int, int] = {0: -1}
running = 0 # 0: sum of no elements yet
# index is the current end. Invariant: running = sum of nums[0..index].
for index, value in enumerate(nums):
running += value
# % k: two prefixes with the same remainder differ by a multiple of k.
remainder = running % k
if remainder not in first_index:
first_index[remainder] = index # earliest only, never overwrite
# The range is first_index + 1 .. index, so its length is the gap.
# >= 2: the problem needs at least two elements.
elif index - first_index[remainder] >= 2:
return True
return False
nums = [23, 2, 4, 6, 7], k = 6:
| index | running | remainder | seen before? | action |
|---|---|---|---|---|
| 0 | 23 | 5 | no | record 5 at 0 |
| 1 | 25 | 1 | no | record 1 at 1 |
| 2 | 29 | 5 | yes, at 0 | 2 - 0 = 2, long enough, return True |
The subarray is [2, 4], which sums to 6.
k.Reading the figure. The rows show nums, the running sum, and the running sum mod 6. The two green cells share remainder 5. The arc joins them. The amber cells between them are the subarray [2, 4], and its sum is 6. The loop returns at index 2, so the dashed cells are never computed.
The map holds at most k distinct remainders, which is a tighter bound than O(n) and worth stating.
% returns a non-negative result for a positive k, so -7 % 6 is 5, not -1. That is exactly what this algorithm needs. In C++ or Java the sign follows the dividend, and you must add k and take the modulo again. Say this; it is a free point.{0: -1} and not {0: 0}. Take [6, 1] with k = 6. At index 0 the remainder is 0, already in the map at -1, and 0 - (-1) = 1, which is too short. Correct: a single 6 does not qualify. With a seed of {0: 0} the arithmetic would be wrong by one and the length test would misfire.False.[0, 0] with any k: True, since 0 is a multiple of everything.[1, 0] with k = 2: False. A good check of the length rule.k: does not qualify, because of the length rule.Return an array where answer[i] is the product of every element except nums[i]. Solve it without division and in O(n) time.
def product_except_self(nums: list[int]) -> list[int]:
"""Product of all elements except the one at each index, without division.
Args:
nums: Integers. Zeros are allowed.
Returns:
A new list where answer[i] is the product of every other element.
Example:
>>> product_except_self([1, 2, 3, 4])
[24, 12, 8, 6]
"""
n = len(nums)
answer = [1] * n # 1: the identity for *, so it changes nothing
# Pass 1, left to right: answer[i] becomes the product of everything
# strictly to the left of i.
prefix = 1 # 1: empty product, nothing to the left of 0
# Invariant: prefix = product of nums[0..i-1] at the top of each pass.
for i in range(n):
answer[i] = prefix # store the left product before nums[i] joins
prefix *= nums[i] # now prefix covers nums[0..i]
# Pass 2, right to left: multiply in the product of everything
# strictly to the right of i.
suffix = 1 # 1: empty product, nothing right of n - 1
# Start at n - 1, the last index. Stop -1 so index 0 is included.
# Step -1 walks backwards.
# Invariant: suffix = product of nums[i+1..n-1] at the top of each pass.
for i in range(n - 1, -1, -1):
answer[i] *= suffix # left product times right product
suffix *= nums[i] # now suffix covers nums[i..n-1]
return answer
nums = [1, 2, 3, 4]:
| i | after pass 1 (left products) | suffix at i | final |
|---|---|---|---|
| 0 | 1 | 24 | 24 |
| 1 | 1 | 12 | 12 |
| 2 | 2 | 4 | 8 |
| 3 | 6 | 1 | 6 |
Reading the figure. Each column is one index. Pass 1 writes the product of everything to the left (blue). Pass 2 walks back and multiplies in the product of everything to the right (violet). The green cell is the result. Both passes start at 1, so the ends of each row are 1. No division is used anywhere.
The output array is not counted as extra space, by the usual convention. Say so, because the problem statement usually says so too.
| Input | Correct answer | What division does |
|---|---|---|
| [1, 2, 0, 4] | [0, 0, 8, 0] | Divides by zero. |
| [0, 0, 3] | [0, 0, 0] | Total is 0, so every entry needs a special case. |
You can patch division by counting the zeros and branching on 0, 1, or more. It works and it is ugly. The two-pass version needs no cases at all, which is the real argument for it.
[1], the empty product. Confirm that is wanted.Return the number of contiguous subarrays whose sum is divisible by k. Values may be negative.
k as you go, so the keys stay in 0..k-1 and the map never grows past k entries.from collections import defaultdict
def subarrays_div_by_k(nums: list[int], k: int) -> int:
"""Count contiguous subarrays whose sum is divisible by k.
Args:
nums: Integers. Negative values are allowed.
k: A positive divisor.
Returns:
The number of qualifying subarrays.
Example:
>>> subarrays_div_by_k([4, 5, 0, -2, -3, 1], 5)
7
"""
# remainder -> how many earlier prefixes had it. Missing keys read as 0.
counts: defaultdict[int, int] = defaultdict(int)
counts[0] = 1 # the empty prefix has remainder 0, seen once
running = 0 # 0: remainder of the empty sum
total = 0 # 0: no subarrays found yet
# Invariant: running = (sum of nums up to and including value) % k.
for value in nums:
# Reducing each step keeps the key in 0..k-1. Python's % is already
# non-negative for a positive k, so negatives need no correction.
running = (running + value) % k
# Each earlier prefix with this remainder closes one valid subarray.
total += counts[running]
counts[running] += 1 # +1: record this prefix after the lookup
return total
nums = [4, 5, 0, -2, -3, 1], k = 5:
| value | running % 5 | counts[r] before | total |
|---|---|---|---|
| 4 | 4 | 0 | 0 |
| 5 | 4 | 1 | 1 |
| 0 | 4 | 2 | 3 |
| -2 | 2 | 0 | 3 |
| -3 | 4 | 3 | 6 |
| 1 | 0 | 1 | 7 |
Answer 7. Note how a remainder seen three times contributes three subarrays at its fourth appearance. That is the counting half doing its job.
k.Reading the figure. The bottom row is every prefix sum mod 5, starting with the seed 0. Cells with the same color hold the same remainder. Any two cells of one color bracket a subarray whose sum divides by 5. So the answer is the number of same-color pairs: 6 amber pairs plus 1 green pair.
m times across the prefix array, including the seeded empty prefix, then it yields m × (m-1) / 2 subarrays, one for each pair. Adding counts[running] before incrementing builds that sum incrementally: 0 then 1 then 2 and so on. Mentioning the closed form shows you see the structure, not just the loop.k, such as [5, 5, 5] with k = 5: every subarray qualifies, so the answer is 6.k == 1: everything is divisible, so the answer is n(n+1)/2.0.sum(nums[i..j]) = P[j+1] - P[i]. The prefix array is one longer than the input, and that leading zero matters.P[j] - P[i] == k into P[i] == P[j] - k. That is a hash lookup, and it is the same move as Two Sum.{0: 1} for counts or {0: -1} for indices.k entries.Pattern 14, Monotonic Stack, answers a different kind of range question: not the aggregate of a range, but where the range ends. For every element, what is the next larger one, and how far away is it?