Precompute a running aggregate so any range answer collapses to a single subtraction: sum(i..j) = P[j+1] − P[i]. Pair the running prefix with a hash map of previously-seen prefixes and you can count or answer subarray questions in O(n) — even with negatives, where a sliding window fails.
A range answer is the difference of two running totals. Add up the array once into a prefix array and sum(i..j) stops being a loop — it becomes P[j+1] − P[i], one subtraction. The moment a problem keeps re-summing overlapping ranges, precompute the total once and subtract.
The brute force for any “sum of nums[i..j]” question re-adds the middle of the range every single time — O(n) per query, O(n²) over many queries. But notice: the sum of a range is just how far the total climbed across it. If you already know the total up to the end and the total up to just before the start, the answer is the gap between them.
nums : [ 3 , 1 , 4 , 1 , 5 , 9 ]
P (sum): 0 3 4 8 9 14 23
index : 0 1 2 3 4 5 6
▲ ▲
P[2] P[5]
sum(nums[2..4]) = 4 + 1 + 5 = 10
= P[5] − P[2] = 14 − 4 = 10
Any range sum = (total up to the end) − (total before the start).
Two array reads. The middle is never re-added.P[j+1] − P[i] — two reads, no re-adding.The big unlock is pairing the running prefix with a map of prefixes you've already seen. “Count subarrays summing to k” rewrites as P[j] − P[i] = k, i.e. P[i] = P[j] − k. As you scan, you ask the map one question — “have I seen the prefix sum − k before?” — and every earlier occurrence is one subarray ending here:
nums = [ 1 , 2 , 3 , -2 , 5 ] k = 3
running sum : 1 3 6 4 9
─────────────────────────
look up sum−k : -2 0 3 1 6 (have I seen this prefix before?)
▲ ▲
sum−k=0 sum−k=1
seen×1 seen×1 → 2 subarrays sum to 3:
[1,2] (sum−k=0 ⇒ from start)
[3,-2,... wait] → [1,2,3,-2,... ]
map seeded with { 0:1 } so a prefix that itself equals k is counted.
A sliding window CAN'T do this: the −2 means growing the window
doesn't monotonically grow the sum. Prefix + map doesn't care about sign.This is the exact same complement-lookup trick as Two Sum, but the keys are running prefix totals rather than raw values.
A sliding window only works when extending the window moves the running total monotonically — growing it can only grow the sum (all-positive), shrinking it can only shrink it. That monotonicity is what lets you safely advance one pointer and never look back.
Add a negative number and that guarantee dies: a longer window can have a smallersum, so you can't decide whether to grow or shrink. The prefix + hash-map approach never relies on monotonicity at all — it just remembers every prefix total it has ever seen and looks up the exact complement. Sign is irrelevant.
Everything in this technique is the same move — carry a running aggregate and reuse it — wearing different clothes. The aggregate can be a sum or a product; you can subtract two of them for a range, or look one up in a map for a count:
keep a running aggregate as you scan left → right:
agg = agg ⊕ nums[i] # ⊕ is + (sum) or × (product) ...
# RANGE form: store agg in P[]; answer any range by subtracting two P's
# COUNT form: need = the complement (agg − k); if it's in the map,
# every earlier occurrence is one valid subarray ending here
# count += map.get(need)
map.set(agg, (map.get(agg) ?? 0) + 1) # file agg for a LATER element
# seed the map with { 0 : 1 } so subarrays starting at index 0 are countedThe product variant (everything except self = prefix product × suffix product) is the same instinct with × instead of +:
nums : [ 2 , 3 , 4 , 5 ]
prefix : 1 2 6 24 product of everything BEFORE i
suffix : 60 20 5 1 product of everything AFTER i
× × × ×
except : 60 40 30 24 prefix[i] × suffix[i]
"all except self" = (everything left of me) × (everything right of me).
The aggregate is a PRODUCT instead of a SUM — same precompute-once move.P[j+1] − P[i], and pair the prefix with a hash map so “count subarrays = k” becomes an O(1) complement lookup (sum − k). Step the Visualize tab to watch the prefix grow, the map fill, and matches flash.Prefix sums attack any problem built on cumulative totals over ranges. The unifying idea: spend O(n) up front to compute a running aggregate, then answer each range or subarray question by reusing it instead of re-summing.
It shows up in three escalating forms:
P; every sum(i..j) is one subtraction. O(1) per query after O(n) setup.i with the product after it — no division needed.It reaches across categories: Kadane's algorithm (Maximum Subarray) is a running aggregate with a reset — a doorway into DP — and the positive-values window contrast lands squarely in Sliding Window.
P[j+1] − P[i], and “does any subarray sum to k?” rewrites as P[j] − P[i] = k — i.e. P[i] = P[j] − k, a hash lookup on prefixes seen before j.That second form is the powerhouse. Carrying the running prefix and a map from prefix total → count of occurrences turns subarray counting into a single O(n) pass. Because it looks up an exact complement rather than relying on the sum being monotonic, it survives negatives that would invalidate a sliding window.
The discipline: seed the map with map.set(0, 1) so a prefix that itself equals k (a subarray starting at index 0) is counted, and file the current prefix after querying so you never match a zero-length range.
Space is O(n) for a prefix array or the prefix-count map. The prefix × suffix product variant can run in O(1) extra space (reusing the output array). The win scales with how many overlapping ranges you'd otherwise re-sum.
P once, subtract per query.i” → multiply the prefix product by the suffix product.The same skeleton generalizes further: 2D prefix sums (integral images) for submatrix sums, prefix XOR for “subarrays with XOR = k”, and prefix-of-differences for range-update / point-query problems (difference arrays).
3. Keep a running prefix total sum and a map of how many times each prefix total has appeared. Seed it with {0: 1} so subarrays starting at index 0 are counted. Have I seen sum − k before?1function subarraySum(nums: number[], k: number): number {2▶ // seen[p] = how many times prefix total p has occurred3▶ const seen = new Map<number, number>([[0, 1]]); // base case4▶ let sum = 0, count = 0;56 for (let i = 0; i < nums.length; i++) {7 sum += nums[i]; // extend running prefix8 const need = sum - k; // complement we hope to have seen9 count += seen.get(need) ?? 0; // every earlier match = one subarray10 seen.set(sum, (seen.get(sum) ?? 0) + 1); // file this prefix for later11 }12 return count;13}
Reach for prefix sums when a problem is about ranges, subarrays, or a running total, especially when you'd otherwise re-sum overlapping ranges. Add the hash map the instant the question becomes count / find subarrays with a sum property — particularly with negatives in the array.
| "count the number of subarrays whose sum equals k" | running prefix + hash map of prefix→count; on each step count += map.get(sum − k) |
| "answer many range-sum queries on a fixed array" | build a prefix array P once; each query is P[j+1] − P[i] in O(1) |
| "product of all elements except self" (no division allowed) | prefix product × suffix product in two passes; O(n) time, O(1) extra space |
| "longest / shortest subarray with a property over a running total" (negatives allowed) | prefix + hash map storing the FIRST index of each prefix total to maximize length |
| "maximum-sum contiguous subarray" | Kadane — a running aggregate that resets to 0 when it would hurt (DP-flavored prefix) |
When → A fixed array with many range-sum queries. Pay O(n) once to build the cumulative array, then answer each sum(i..j) in O(1).
// 1. Build a prefix array once — O(n)
// P[i] = sum of nums[0..i-1]; P[0] = 0 (empty prefix)
function buildPrefix(nums: number[]): number[] {
const P = new Array<number>(nums.length + 1).fill(0);
for (let i = 0; i < nums.length; i++) P[i + 1] = P[i] + nums[i];
return P;
}
// 2. Any range sum is now ONE subtraction — O(1) per query
// sum of nums[i..j] (inclusive) = P[j + 1] - P[i]
function rangeSum(P: number[], i: number, j: number): number {
return P[j + 1] - P[i];
}P[0] = 0 and P[i+1] = P[i] + nums[i], the inclusive range nums[i..j] is P[j+1] − P[i]. The +1 offset is what lets a range starting at index 0 subtract the empty prefix.When → Count or locate subarrays whose sum satisfies a property — the textbook subarray-sum-equals-k. The map makes the complement lookup O(1), and it tolerates negatives.
// Count subarrays summing to k — O(n), works WITH negatives.
function subarraySum(nums: number[], k: number): number {
// seen[p] = how many times prefix total p has occurred so far
const seen = new Map<number, number>([[0, 1]]); // base case is mandatory
let sum = 0, count = 0;
for (const n of nums) {
sum += n; // extend running prefix
// a subarray ending here sums to k ⇔ some earlier prefix = sum - k
count += seen.get(sum - k) ?? 0; // each earlier match = 1 subarray
seen.set(sum, (seen.get(sum) ?? 0) + 1); // file this prefix for the future
}
return count;
}[[0, 1]] (so a prefix equal to k counts), and update count before filing the current prefix (so you never count a zero-length subarray). For longest subarray variants, store each prefix's first index instead of a count.When → Combine everything except position i — classically product of array except self without division. Generalizes the prefix idea from + to ×.
// Product of array except self — no division, O(n) time, O(1) extra space.
function productExceptSelf(nums: number[]): number[] {
const n = nums.length;
const res = new Array<number>(n).fill(1);
// 1. Left→right: res[i] = product of everything BEFORE i (prefix product)
let prefix = 1;
for (let i = 0; i < n; i++) {
res[i] = prefix;
prefix *= nums[i];
}
// 2. Right→left: multiply in the product of everything AFTER i (suffix product)
let suffix = 1;
for (let i = n - 1; i >= 0; i--) {
res[i] *= suffix;
suffix *= nums[i];
}
return res;
}The prefix-count map must start with map.set(0, 1). Without it, any subarray that starts at index 0 and sums to k is missed — there's no “previous prefix” entry equal to 0 to match the running sum against. This is the single most common bug in subarray-sum-equals-k.
With the convention P[0] = 0 and P[k] = nums[0] + … + nums[k−1], the inclusive range nums[i..j] is P[j+1] − P[i] — not P[j] − P[i]. Mixing the 0-based array index with the 1-based prefix index drops the first or last element. Pick one convention and write the formula down.
A sliding window relies on the running sum being monotonic: growing the window can only grow the sum, so you can advance one pointer and never backtrack. A single negative number breaks that — a longer window may have a smaller sum — so the window can't decide whether to grow or shrink. Prefix + hash map looks up the exact complement and doesn't care about sign, which is why it's the correct tool for subarray-sum-equals-k with negatives.
A running prefix over up to 10⁵ elements with large values can exceed a 32-bit integer, and products blow up far faster. In JavaScript/TypeScript, numbers are IEEE-754 doubles (safe integers to 2⁵³) so sums are usually fine, but in typed languages use a 64-bit type, and for products consider BigInt or modular arithmetic. Always read the constraints before assuming the aggregate fits.