Prefix Sum
Precompute cumulative totals so any contiguous range sum is one subtraction, and combine them with a hash map to count or locate subarrays with a target sum, remainder or balance in O(n).
Instinct Repeated range-sum queries, or "count subarrays whose sum equals / divides K" with negatives allowed? Store running totals and subtract two of them.
Memory hack Any range is two prefixes apart - sum(i..j) = P[j+1] - P[i], so remember every P you've passed.
At a glance
When to use it
- Many range-sum queries are asked on an array that does not change
- You must count subarrays whose sum equals k and negatives are allowed
- The condition is about divisibility by k or a balance of two kinds of element
- You need the longest subarray with a given sum or balance
Key idea
Store running totals so any range sum is the difference of two prefixes, and keep earlier prefixes in a hash map so each index finds its matching partner in O(1), turning an O(n^2) subarray scan into one O(n) pass.
Typical complexity
O(n) time, O(n) space (O(1) per range query after building)
| # | Common problems | Level | Our solution | Practice |
|---|---|---|---|---|
| 1 | Range Sum Query - Immutable | Easy | Read solution | LeetCode ↗ |
| 2 | Subarray Sum Equals K | Medium | Read solution | LeetCode ↗ |
| 3 | Subarray Sums Divisible by K | Medium | Read solution | LeetCode ↗ |
| 4 | Continuous Subarray Sum | Medium | Read solution | LeetCode ↗ |
| 5 | Contiguous Array | Medium | Read solution | LeetCode ↗ |
The instinct
A prefix sum array turns a range question into a difference of two points. Define P[0] = 0 and P[i+1] = P[i] + a[i]. Then the sum of a[l..r] is P[r+1] - P[l], answered in O(1) after O(n) preprocessing.
The bigger interview idea is the prefix + hash map combo: "a subarray ending here has sum k" is the same as "some earlier prefix equals current - k". So while scanning, you ask a map how many earlier prefixes had that value. This works with negative numbers, which is exactly where sliding window breaks down.
Recognise it
| Signal in the problem | Why it points here |
|---|---|
| "Sum of elements between indices i and j", many queries | Precompute once, answer each query with one subtraction. |
| "Number of subarrays whose sum equals k" (negatives allowed) | Count earlier prefixes equal to prefix - k. |
| "Subarray sum divisible by k" / "multiple of k" | Two prefixes with the same remainder bound a divisible subarray. |
| "Equal number of 0s and 1s", "balanced" | Map 0 to -1; a balanced subarray has prefix difference 0. |
| "Longest subarray with sum k" | Store the first index where each prefix value appears. |
| "Product/sum of everything except i", "left part vs right part" | Prefix from the left combined with suffix from the right. |
| "Add v to every element in [l, r]" many times | Difference array: the inverse of prefix sums. |
Not this pattern if...
- All numbers are non-negative and you want the shortest/longest window meeting a threshold: a Sliding Window uses O(1) space.
- You want the maximum subarray sum with no target: Kadane's Running Best is simpler.
- The array changes between queries (point updates + range queries): use a Fenwick tree or segment tree.
Memory hack
Any range is two prefixes apart. sum(i..j) = P[j+1] - P[i], so remember every P you've passed.
Think of a car's odometer: to know how far you drove between two towns, you don't re-drive the road, you subtract the two odometer readings. The hash map is your logbook of past readings, and {0: 1} is the reading before you left home.
Say it out loud in the interview:
- "Range sum = current prefix minus an earlier prefix."
- "So I need an earlier prefix equal to
current - target(or with the same remainder)." - "Seed the map with prefix 0, query first, then record the current prefix."
How it works
For counting subarrays with sum k, scan once with a running sum. At each index, the number of subarrays ending here with sum k equals how many times sum - k appeared among earlier prefixes.
Trace for Subarray Sum Equals K on [1, 2, 3], k = 3:
| i | a[i] | prefix | need = prefix - 3 | seen before | count | map after |
|---|---|---|---|---|---|---|
| start | 0 | 0 | {0:1} | |||
| 0 | 1 | 1 | -2 | 0 | 0 | {0:1, 1:1} |
| 1 | 2 | 3 | 0 | 1 | 1 | {0:1, 1:1, 3:1} |
| 2 | 3 | 6 | 3 | 1 | 2 | {0:1, 1:1, 3:1, 6:1} |
The two subarrays are [1, 2] and [3]. Notice that [1, 2] was found only because the map was seeded with {0: 1}.
Remainder trick: if P[j] % k == P[i] % k, then (P[j] - P[i]) is divisible by k. Subarray Sums Divisible by K counts equal remainders; Continuous Subarray Sum stores the first index of each remainder and checks the length is at least 2.
Balance trick: Contiguous Array replaces 0 with -1, then looks for the longest gap between two equal prefix values.
Java template
import java.util.HashMap; import java.util.Map; class PrefixSumToolkit { // 1) Static range queries: build once, query in O(1). static long[] buildPrefix(int[] a) { long[] p = new long[a.length + 1]; // p[0] = 0 sentinel for (int i = 0; i < a.length; i++) p[i + 1] = p[i] + a[i]; return p; } static long rangeSum(long[] p, int l, int r) { // inclusive l..r return p[r + 1] - p[l]; } // 2) Count subarrays whose key matches (sum == k here). static int countSubarrays(int[] a, int k) { Map<Long, Integer> seen = new HashMap<>(); seen.put(0L, 1); // empty prefix long sum = 0; int count = 0; for (int x : a) { sum += x; // problem-specific transform of x count += seen.getOrDefault(sum - k, 0); // query first seen.merge(sum, 1, Integer::sum); // then record } return count; } // 3) Longest subarray with a property: store FIRST index of each key. static int longestBalanced(int[] bits) { Map<Integer, Integer> firstAt = new HashMap<>(); firstAt.put(0, -1); // prefix before index 0 int bal = 0, best = 0; for (int i = 0; i < bits.length; i++) { bal += bits[i] == 1 ? 1 : -1; // or ((sum % k) + k) % k for remainders Integer j = firstAt.putIfAbsent(bal, i); if (j != null) best = Math.max(best, i - j); } return best; } }
Complexity
| Task | Time | Space |
|---|---|---|
| Build prefix array | O(n) | O(n) |
| Each range query | O(1) | |
| Prefix + hash map counting | O(n) average | O(n) |
| Remainder version | O(n) | O(min(n, k)) |
| 2D prefix (matrix region sums) | O(rows × cols) build, O(1) query | O(rows × cols) |
Variations
- Immutable range queries: Range Sum Query - Immutable; the 2D version uses inclusion-exclusion on a matrix.
- Count by target difference: Subarray Sum Equals K.
- Count by equal remainder: Subarray Sums Divisible by K.
- Existence by remainder with a length constraint: Continuous Subarray Sum (first index per remainder).
- Longest by balance: Contiguous Array (0 becomes -1).
- Prefix XOR / prefix parity: same idea with XOR, or with bitmasks of odd/even counts.
- Difference array: apply many range increments with two point updates, then prefix-sum once.
Common mistakes
- Forgetting the seed (
{0: 1}for counting,{0: -1}for longest length), which misses subarrays that start at index 0. - Recording the current prefix before querying, which can match an empty subarray (wrong when k = 0).
- Overwriting the first index of a key when you want the longest subarray; use
putIfAbsent. - Integer overflow in long arrays of large values; accumulate in
long. - Off-by-one between
P[r+1] - P[l]andP[r] - P[l-1]; pick one convention and stick to it.
In Java, % keeps the sign of the dividend, so -1 % 5 == -1. With negative numbers, two prefixes that are congruent mod k can produce different keys. Normalise with ((sum % k) + k) % k before using a remainder as a map key.
All practice problems
5 problems on this site use this pattern, easiest first. Solve them in order and say the instinct out loud before you code.