Prefix Sums & Running Aggregates

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.

Technique4 problems
The unlock

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.

MENTAL MODEL A range is a difference of running totals

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.
The reframe →don't sum the range, subtract two prefixes. Compute the running total once; every range query is then P[j+1] − P[i] — two reads, no re-adding.

SEE IT The hash-map twist: count subarrays in one pass

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.

The smell test →“count / longest / shortest subarray with a property over a running total” and the array has negatives? That's prefix + hash map, not a window.

SAY IT Why negatives break the window but not the prefix map

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.

Decision rule → all-positive values + a longest/shortest length target → a sliding window is simpler and O(1) space. Negatives present, or you need an exact count → reach for prefix + hash map.

SHAPE One running aggregate, three guises

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 counted

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

MNEMONIC Range = difference of prefixes. Count = seen a prefix before?

“Subtract two totals, or look one up.” Precompute a running aggregate so a range answer is 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.

PATTERN What this technique is really about

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:

  • 1D prefix array → range queries. Precompute P; every sum(i..j) is one subtraction. O(1) per query after O(n) setup.
  • Running prefix + hash map → subarray counts.Carry the prefix and a map of prefixes seen; “count subarrays = k” becomes a complement lookup. Works with negatives, where sliding windows can't.
  • Prefix × suffix → “all except self”. The aggregate is a product; combine the product before 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.

KEY IDEA Range = difference of prefixes; count = "seen prefix" hashing

The insight → instead of re-summing a range, store the running total at every boundary. A range sum is then 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.

COST Complexity profile

Re-sum each range
O(n²)
Every query re-adds its whole range.
Prefix (+ hash map)
O(n)
O(n) precompute, then O(1) per query / one pass.

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.

VARIANTS The three forms at a glance

  • 1D prefix array. Static array, many range-sum queries → build P once, subtract per query.
  • Prefix + hashmap. Count / locate subarrays with a sum (or modular, or parity) property over a running total → complement lookup on seen prefixes.
  • Prefix × suffix products. “Combine everything except position 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).

RUN IT Have I seen sum − k before?

step 0 / 16
STARTCount subarrays summing to 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 occurred
3 const seen = new Map<number, number>([[0, 1]]); // base case
4 let sum = 0, count = 0;
5
6 for (let i = 0; i < nums.length; i++) {
7 sum += nums[i]; // extend running prefix
8 const need = sum - k; // complement we hope to have seen
9 count += seen.get(need) ?? 0; // every earlier match = one subarray
10 seen.set(sum, (seen.get(sum) ?? 0) + 1); // file this prefix for later
11 }
12 return count;
13}
nums102132-2354
State
3
k
0
sum
need (sum−k)
0
count
{0:1}
seen (prefix→#)
current elementalready processedrunning sumcomplement (sum − k)count
slowfast

TRIGGERS When you see ___ → reach for ___

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)

RED FLAGSWhen it's NOT this pattern

  • All values are positive and you want the longest/shortest subarray. A sliding window is simpler and runs in O(1) extra space — the monotonic growth of the sum lets you advance one pointer and never backtrack. Minimum Size Subarray Sum is the canonical case. Prefix + hash map still works but is overkill here.
  • The array mutates between queries. A plain prefix array is invalidated by any update. If you need range queries and point updates, reach for a Fenwick tree (BIT) or segment tree instead.
  • You only ever ask one range sum. Building a whole prefix array to answer a single query is wasted O(n) space — just sum that one range.

TEMPLATE Prefix array + range query

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

prefix-array-range-query.ts
// 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];
}
Mind the off-by-one → with 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.

TEMPLATE Running prefix + hashmap of counts

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.

running-prefix-hashmap-of-counts.ts
// 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;
}
Two non-negotiables → seed the map with [[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.

TEMPLATE Prefix × suffix products

When → Combine everything except position i — classically product of array except self without division. Generalizes the prefix idea from + to ×.

prefix-suffix-products.ts
// 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;
}
O(1) extra space →write prefix products straight into the output array on the way right, then fold in suffix products on the way back with a single scalar. The output array doesn't count against extra space by the usual convention.

PITFALL Forgetting to seed the map with { 0: 1 }

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.

PITFALL Off-by-one between P[i] and P[i+1]

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.

PITFALL Reaching for a sliding window when the array has negatives

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.

PITFALL Integer overflow on large running sums or products

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.