~/searching/prefix-sums
Prefix sums
Add up an array once, left to right. After that, the sum of any range is one subtraction.
Store pre[i] = the sum of the first i elements. The sum of a[l..r] is then pre[r + 1] - pre[l].
Many range-sum questions on an array that doesn't change, or counting subarrays with a given sum.
O(n) to build, O(1) per query
O(n)
You’ll recognise it when
- You’re asked for the sum of a range
a[l..r]many times, and the array doesn’t change between questions. - You need to count subarrays whose sum is exactly
k, and the numbers can be negative. - Many updates each add a value to a whole range, and you only read the result at the end.
- The brute force adds up the same elements again and again: O(n) per question, O(n · q) in total.
It’s often confused with the sliding window. A window only moves forward and needs a rule that behaves as it grows. Prefix sums answer any range, in any order, and don’t care about negative numbers.
The idea
Think of a car’s odometer. To know how far you drove between two towns, you don’t add up every stretch of road in between. You read the odometer at both towns and subtract.
A prefix sum array is an odometer for your array. Walk along once and write down the running total before each element: pre[i] is the sum of the first i elements. The sum of any stretch is then the reading at its end minus the reading at its start. One pass to build, then every range costs one subtraction.
How it works
pre has one more cell than the array, and pre[i] covers a[0..i-1]. In the graphic, each pre[i] sits on the boundary just before a[i]: it’s the sum of everything to its left.
Scroll through the steps and the graphic follows along. You can also press play, step with the arrow keys, or edit the array and the queries: try l = r, or the whole array.
- Make
preone cell longer than the array and setpre[0] = 0, the sum of no elements. - Go left to right:
pre[i + 1] = pre[i] + a[i]. Each cell is the one before it plus one more element. - Negative numbers just make the running total go down. Nothing else changes.
- After one pass,
pre[i]is the sum of the firstielements, andpre[n]is the total. - A query asks for
sum(l, r), the sum ofa[l..r]with both ends included. pre[r + 1]holds everything up to and includinga[r], andpre[l]holds everything beforea[l]. Subtract:sum(l, r) = pre[r + 1] - pre[l]. The two cells sit exactly on the edges of the range.- When
l = 0, the formula usespre[0] = 0, so a range that starts at the front needs no special case. That’s what the extra cell is for. - Every query was one subtraction, no matter how long its range.
Why it’s correct: pre[r + 1] is a[0] + … + a[l-1] + a[l] + … + a[r], and pre[l] is a[0] + … + a[l-1]. The shared front part cancels, and what’s left is exactly a[l..r].
def build_prefix(a):
"""pre[i] = a[0] + ... + a[i-1], so pre has len(a) + 1 entries."""
pre = [0] * (len(a) + 1)
for i, x in enumerate(a):
pre[i + 1] = pre[i] + x
return pre
def range_sum(pre, l, r):
"""Sum of a[l..r], both ends included, in O(1)."""
return pre[r + 1] - pre[l]#include <vector>
using namespace std;
// pre[i] = a[0] + ... + a[i-1], so pre has a.size() + 1 entries.
// 64-bit sums: the total can pass 2^31 even when every value fits in an int.
vector<long long> buildPrefix(const vector<long long>& a) {
vector<long long> pre(a.size() + 1, 0);
for (size_t i = 0; i < a.size(); i++) {
pre[i + 1] = pre[i] + a[i];
}
return pre;
}
// Sum of a[l..r], both ends included, in O(1).
long long rangeSum(const vector<long long>& pre, int l, int r) {
return pre[r + 1] - pre[l];
}class PrefixSums {
// pre[i] = a[0] + ... + a[i-1], so pre has a.length + 1 entries.
// 64-bit sums: the total can pass 2^31 even when every value fits in an int.
static long[] buildPrefix(long[] a) {
long[] pre = new long[a.length + 1];
for (int i = 0; i < a.length; i++) {
pre[i + 1] = pre[i] + a[i];
}
return pre;
}
// Sum of a[l..r], both ends included, in O(1).
static long rangeSum(long[] pre, int l, int r) {
return pre[r + 1] - pre[l];
}
}Counting subarrays that sum to k
The same subtraction answers a harder question: how many subarrays sum to exactly k? A subarray ending at index j sums to k when some earlier prefix equals pre[j + 1] - k. So walk once, keep a hash map from each prefix value to how many times you’ve seen it, and look up running - k before adding the current prefix:
from collections import Counter
def count_subarrays(a, k):
seen = Counter({0: 1}) # the empty prefix, pre[0] = 0
running = count = 0
for x in a:
running += x
count += seen[running - k]
seen[running] += 1
return count
print(count_subarrays([1, -1, 1, 1], 1)) # 5
That’s O(n) with negatives allowed, which is exactly where a sliding window fails. The {0: 1} seed counts subarrays that start at index 0.
Why it’s O(n) to build and O(1) per query
Building pre touches each element once: n additions. A query reads two cells and subtracts, whatever the length of the range. So q queries cost O(n + q) in total, against O(n · q) for adding each range up from scratch.
| n | q | add up each range | prefix sums |
|---|---|---|---|
| 1,000 | 1,000 | up to 1,000,000 | about 2,000 |
| 100,000 | 100,000 | up to 10¹⁰ | about 200,000 |
Space is O(n) for the n + 1 cells of pre. The catch: if the array changes, every later prefix changes too. For updates mixed with queries, use a Fenwick tree or a segment tree instead.
Common mistakes
Dropping the last element
With pre one cell longer than the array, the inclusive range a[l..r] needs pre[r + 1]. Using pre[r] quietly leaves out a[r], and single-element queries come out as 0.
total = pre[r] - pre[l] # ✗ sums a[l..r-1]
total = pre[r + 1] - pre[l] # ✓ sums a[l..r]
Making pre the same length as the array
If pre[i] includes a[i] itself, a range starting at 0 needs pre[l - 1], which doesn’t exist. You end up writing an if l == 0 branch, and forgetting it reads pre[-1], which in Python is silently the last element. The extra leading 0 removes the special case.
Overflowing 32-bit integers
Values fit in an int, but their sum might not: 10⁵ values of 10⁹ add up to 10¹⁴. In C++ and Java, store pre as 64-bit. Python’s integers never overflow.
vector<int> pre(n + 1); // ✗ wraps around silently
vector<long long> pre(n + 1); // ✓ fits sums up to about 9·10¹⁸
Forgetting the {0: 1} seed
When counting subarrays that sum to k, a subarray starting at index 0 matches the empty prefix, pre[0] = 0. Start the map empty and every such subarray is missed.
seen = Counter() # ✗ misses subarrays that start at 0
seen = Counter({0: 1}) # ✓ the empty prefix counts once
Variations
- 2-D prefix sums. For a grid, let
S[i][j]be the sum of the rectangle of rows0..i-1and columns0..j-1. Build it withS[i+1][j+1] = g[i][j] + S[i][j+1] + S[i+1][j] - S[i][j]. A rectangle from(r1, c1)to(r2, c2)isS[r2+1][c2+1] - S[r1][c2+1] - S[r2+1][c1] + S[r1][c1]: take the big corner, cut off the strip above and the strip to the left, then add back the piece you cut twice. - Difference arrays. The reverse trick, for many “add
vtoa[l..r]” updates followed by one read. Record each update asd[l] += vandd[r + 1] -= v(sodneedsn + 1cells), then take the prefix sums ofdonce to get the final values. That’s O(n + q) instead of O(n · q). - Other operations. Anything with an undo works the same way: prefix XOR (undo with XOR), prefix counts of a letter or a condition (how many vowels in
s[l..r]?). Minimum and maximum have no undo, so they need a sparse table or a segment tree. - Prefix sums plus binary search. With non-negative numbers,
preis sorted, so you can binary search it: the first index where the running total reaches a target, or a weighted random pick (draw a number below the total and find which cell it falls in). - Subarray sums with a hash map. Beyond “sum equals
k”, the same map of earlier prefixes finds subarrays whose sum is divisible byk(store prefixes modk), or the longest subarray with sumk(store each prefix’s first index instead of a count).
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
Count the subarrays whose sum is exactly
k. The array has up to 10⁵ numbers, and some are negative. Which approach fits?A subarray sums to
kexactly when two prefixes differ byk, so counting earlier prefixes equal torunning - kfinds them all in O(n). With negatives, a window’s sum can go down as it grows, so shrinking can skip answers, and the prefix array isn’t sorted, so binary search doesn’t apply. Sorting destroys which elements are contiguous. -
2
What does this print?
a = [2, 7, -3, 5, 1]pre = [0] * (len(a) + 1)for i, x in enumerate(a):pre[i + 1] = pre[i] + xprint(pre[4] - pre[1], pre[5] - pre[0])pre[i]is the sum of the firstielements, sopre[4] - pre[1]isa[1] + a[2] + a[3] = 7 - 3 + 5 = 9, andpre[5] - pre[0]is the whole array, 12. Readingpre[4] - pre[1]asa[1..4]gives 10, and asa[1..2]gives 4: the usual off-by-ones. 11 ispre[4]alone. -
3
What does this print?
n = 6d = [0] * (n + 1)for l, r, v in [(0, 2, 5), (2, 5, 1), (4, 4, -3)]:d[l] += vd[r + 1] -= vout, run = [], 0for i in range(n):run += d[i]out.append(run)print(out)Each update writes only two cells; the prefix-sum pass spreads it over its range. +5 on 0..2, +1 on 2..5 and -3 on 4 give
[5, 5, 6, 1, -2, 1]. Index 2 is in both of the first two ranges, so it gets 6, and the -3 is cancelled again at index 5. -
4
This should count subarrays with sum
k. What does it print, and why?from collections import Counterdef count(a, k):seen = Counter()running = total = 0for x in a:running += xtotal += seen[running - k]seen[running] += 1return totalprint(count([3, 1, 2], 3))[1, 2]is found because the prefix 3 was stored before the running total reached 6. But[3]starts at index 0, so it needs the empty prefix 0, which was never put in the map. SeedingCounter({0: 1})fixes it. The lookups aren’t all empty: only the ones that need the empty prefix fail. -
5
S[i][j]is the sum of the grid cells in rows0..i-1and columns0..j-1. Which gives the sum of the rectangle from(r1, c1)to(r2, c2), both corners included?Take everything up to the bottom-right corner, cut off the strip above row
r1and the strip left of columnc1, then add back the top-left block, which was cut twice. The second choice forgets the +1 and loses rowr2and columnc2. The third also cuts rowr1and columnc1, which belong to the rectangle. The last subtracts the corner block a third time instead of adding it back.