Prefix sums
In short: Prefix sums are computed once in O(n), and after that the sum of any range from a to b is a single subtraction of two prefix sums.
Problem
The dashboard that sums every time
The shop dashboard shows the revenue for a chosen period: “from day 120 to day 340”, “the last 30 days”, “this week against last week”. Managers open it many times a day, and every chart asks about dozens of periods. The code sums the days of every period again:
static long RangeSum(int[] revenue, int a, int b){ long sum = 0; for (int day = a; day <= b; day++) { sum += revenue[day]; } return sum;}A thousand days of revenue and a thousand queries:
int[] revenue = RevenueOf(1000);var queries = QueriesOf(1000);long total = 0;long additions = 0;foreach (var (a, b) in queries){ total += RangeSum(revenue, a, b); additions += b - a + 1;}Console.WriteLine($"queries: {queries.Length}");Console.WriteLine($"sum of the answers: {total}");Console.WriteLine($"additions: {additions}");queries: 1000sum of the answers: 68412493additions: 248794248,794 additions for a thousand answers. The days do not change between two queries, yet the code adds them up again and again. The longer the periods and the more queries there are, the slower the dashboard gets.
Where the days and the queries come from
The revenue and the queries follow a formula, so that the programs in all four languages and the comparison in the browser below build exactly the same ones:
static int[] RevenueOf(int n){ var revenue = new int[n]; for (int i = 0; i < n; i++) { revenue[i] = 50 + (i * 37) % 451; } return revenue;}
static (int A, int B)[] QueriesOf(int n){ var queries = new (int A, int B)[n]; for (int k = 0; k < n; k++) { int a = (k * 7919) % n; queries[k] = (a, a + (k * 4231) % (n - a)); } return queries;}The idea
The sum of everything to the left
Compute once, for every day, the revenue from the start up to that day. This is a prefix sum: prefix[k] is the sum of the first k days.
prefix[0] = 0: the sum of no days at all;prefix[k + 1] = prefix[k] + revenue[k]: every place is the previous one plus one day.
The array has one more place than there are days. After that, every period is the difference of two numbers. The revenue from day a to day b is days 0 to b minus days 0 to a − 1:
prefix[b + 1] − prefix[a]
Building it is n additions, once. Every query after that is two reads and one subtraction, whether the period is two days or two years: O(1) per query instead of O(n).
See it
The prefixes, day by day
static long[] BuildPrefix(int[] revenue){ var prefix = new long[revenue.Length + 1]; for (int i = 0; i < revenue.Length; i++) { prefix[i + 1] = prefix[i] + revenue[i]; } return prefix;}
static long RangeSum(long[] prefix, int a, int b){ return prefix[b + 1] - prefix[a];}int[] revenue = { 120, 80, 150, 90, 200, 60, 110, 140 };long[] prefix = BuildPrefix(revenue);Console.WriteLine($"prefix: [{string.Join(", ", prefix)}]");Console.WriteLine($"days 2–5: {RangeSum(prefix, 2, 5)}");prefix: [0, 120, 200, 350, 440, 640, 700, 810, 950]days 2–5: 500static long[] BuildPrefix(int[] revenue){ var prefix = new long[revenue.Length + 1]; for (int i = 0; i < revenue.Length; i++) { prefix[i + 1] = prefix[i] + revenue[i]; } return prefix;}
static long RangeSum(long[] prefix, int a, int b){ return prefix[b + 1] - prefix[a];}Step by step, in words
- 8 days of revenue. prefix has 9 places: prefix[0] = 0 is the sum of no days at all.
- prefix[1] = prefix[0] + revenue[0] = 0 + 120 = 120: days 0–0.
- prefix[2] = prefix[1] + revenue[1] = 120 + 80 = 200: days 0–1.
- prefix[3] = prefix[2] + revenue[2] = 200 + 150 = 350: days 0–2.
- prefix[4] = prefix[3] + revenue[3] = 350 + 90 = 440: days 0–3.
- prefix[5] = prefix[4] + revenue[4] = 440 + 200 = 640: days 0–4.
- prefix[6] = prefix[5] + revenue[5] = 640 + 60 = 700: days 0–5.
- prefix[7] = prefix[6] + revenue[6] = 700 + 110 = 810: days 0–6.
- prefix[8] = prefix[7] + revenue[7] = 810 + 140 = 950: days 0–7.
- 8 additions, once. Now the query: the revenue from day 2 to day 5. prefix[6] = €700 is days 0–5.
- prefix[2] = €200 is days 0–1, the part before the period. Take it away.
- 700 − 200 = €500: days 2–5 with two reads and one subtraction, however long the period is.
- Building costs n additions once; every query after that costs one subtraction.
In the scene prefix[k] sits on the boundary between day k − 1 and day k. That shows it is the sum of everything to the left of the boundary, and a period is the difference between two boundaries.
More precisely, if you are curious
What the languages give you.
C# has no ready-made prefix sums, neither on arrays nor in LINQ. The loop above is the usual way.Overflow. prefix is long, not int. The last prefix is the revenue for the whole period. In euro cents over a few years it passes 2,147,483,647, and an int then silently becomes a negative number.
Try it
Predict prefix
Three days of revenue: €30, €50 and €20.
int[] revenue = { 30, 50, 20 };long[] prefix = BuildPrefix(revenue);Console.WriteLine($"prefix: [{string.Join(", ", prefix)}]");prefix[0] is 0. Then 0 + 30, 30 + 50 and 80 + 20. The array has four places for three days.
Try it
Fill in
The revenue from day a to day b inclusive:
return prefix[] - prefix[a];prefix[b + 1] is the sum of days 0 to b, and prefix[a] is the sum of days 0 to a − 1. The difference is the period with both ends.
The idea
The zero in front
Why does the array have one more place? It can do without: prefix[k] could be the sum of days 0 to k, inclusive. Then the period from a to b is prefix[b] − prefix[a − 1]. But for a = 0 there is no place a − 1, and every query needs a check “if a is 0”.
The zero in front removes that check. The price is one place in the array, and the trap is that the indexes shift by one: prefix[k] is the sum of the days before k, not up to k. That is why an off-by-one error is the most common mistake here, as with binary search.
Try it
Find the bug
Someone wrote the prefixes without the zero in front. The query for days 2–5 should give €500:
int[] revenue = { 120, 80, 150, 90, 200, 60, 110, 140 };long[] prefix = BuildPrefix(revenue);Console.WriteLine($"days 2–5: {RangeSum(prefix, 2, 5)}");days 2–5: 350Pick the line with the bug.
Here prefix[k] is the sum of days 0 to k, inclusive. Then prefix[5] − prefix[2] is days 3 to 5, without day 2: 350 instead of 500. Without the zero in front you need prefix[b] − prefix[a − 1] and a separate case for a = 0. With the zero in front (new long[n + 1]) it stays prefix[b + 1] − prefix[a] with no special cases.
See it
Measure: re-summing against the prefixes
The same thousand queries, with the prefixes:
int[] revenue = RevenueOf(1000);var queries = QueriesOf(1000);long[] prefix = BuildPrefix(revenue);long operations = revenue.Length;long total = 0;foreach (var (a, b) in queries){ total += RangeSum(prefix, a, b); operations++;}Console.WriteLine($"queries: {queries.Length}");Console.WriteLine($"sum of the answers: {total}");Console.WriteLine($"additions and subtractions: {operations}");queries: 1000sum of the answers: 68412493additions and subtractions: 2000The sum of the answers is the same, and the work is 2,000 operations instead of 248,794: a thousand additions for prefix and one subtraction per query. Run both in the browser. n is both the number of days and the number of queries:
- Re-summing for every query
O(n²) - Prefix sums
O(n)
| Algorithm | n = 10 | n = 1,000 | n = 100,000 |
|---|---|---|---|
| Re-summing for every query | 10 | 248,794 | ≈ 2,487,940,000 |
| Prefix sums | 20 | 2,000 | 200,000 |
static long[] BuildPrefix(int[] revenue){ var prefix = new long[revenue.Length + 1]; for (int i = 0; i < revenue.Length; i++) { prefix[i + 1] = prefix[i] + revenue[i]; } return prefix;}
static long RangeSum(long[] prefix, int a, int b){ return prefix[b + 1] - prefix[a];}Re-summing is O(n) per query, O(n²) for n queries. With the prefixes it is O(n) once and O(1) per query. The price is memory: one array with n + 1 numbers.
Two-dimensional prefix sums: orders by day and hour
The heat map of orders is a table: the rows are days of the week, the columns are hours. The question is “how many orders are there on Tuesday and Wednesday from 10:00 to 13:00”, that is, the sum inside a rectangle.
p[r][c] is the sum of the rectangle from the top left corner to row r − 1 and column c − 1, again with zeros in front (row 0 and column 0). Every place is the cell plus the rectangle above and the one to the left. The corner between them was counted twice, so it is taken away once:
static int[,] BuildPrefix2D(int[,] orders){ int rows = orders.GetLength(0); int cols = orders.GetLength(1); var p = new int[rows + 1, cols + 1]; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { p[r + 1, c + 1] = orders[r, c] + p[r, c + 1] + p[r + 1, c] - p[r, c]; } } return p;}
static int Rect(int[,] p, int r1, int c1, int r2, int c2){ return p[r2 + 1, c2 + 1] - p[r1, c2 + 1] - p[r2 + 1, c1] + p[r1, c1];}int[,] orders = { { 3, 8, 12, 9, 5, 4 }, { 2, 7, 14, 11, 6, 3 }, { 4, 9, 15, 10, 7, 5 }, { 1, 6, 11, 8, 4, 2 },};var p = BuildPrefix2D(orders);Console.WriteLine($"orders Tue–Wed from 10:00 to 13:00: {Rect(p, 1, 1, 2, 3)}");orders Tue–Wed from 10:00 to 13:00: 66The query is the same idea: the whole rectangle up to the bottom right corner, minus the strip above, minus the strip to the left, plus the corner that was taken away twice. Four reads for a rectangle of any size.
The idea
How many periods make exactly €50
The shop has days with a loss: returned orders are taken off the revenue. The question is how many periods of consecutive days have a profit of exactly €50.
Every period from day i to day j is the difference of two prefixes: prefix[j + 1] − prefix[i]. It is 50 exactly when prefix[i] = prefix[j + 1] − 50. So you walk through the days, keep the current prefix and ask: how many times so far have I seen a prefix equal to the current one minus 50? The answer is kept in a map from prefix to how many times it was seen:
static int CountPeriods(int[] profit, int target){ var seen = new Dictionary<int, int> { [0] = 1 }; int prefix = 0; int count = 0; foreach (int p in profit) { prefix += p; count += seen.GetValueOrDefault(prefix - target); seen[prefix] = seen.GetValueOrDefault(prefix) + 1; } return count;}int[] profit = { 40, -10, 30, 20, -30, 50, -20, 30, 0, 20 };Console.WriteLine($"periods with a profit of €50: {CountPeriods(profit, 50)}");periods with a profit of €50: 8One pass over the days and one lookup in a hash table per day: O(n). Two nested loops over all periods would be O(n²). The map here is a counter: how many times each value was seen. That is a technique of its own, and it is the subject of the next lesson.
More precisely, if you are curious
seen starts with 0 → 1, because the prefix before the first day is 0. Without it the periods that start on day 0 are not counted.
If every day made a profit, a sliding window would also work: grow it on the right while the sum is under 50, shrink it on the left once it goes over. With negative numbers that fails: shrinking can increase the sum, and the window cannot tell which way to go. Prefixes with a map work with any numbers.
Try it
Predict the count
Four days of profit: €20, €30, −€30 and €30. How many periods of consecutive days make exactly €50?
int[] profit = { 20, 30, -30, 30 };Console.WriteLine($"periods with a profit of €50: {CountPeriods(profit, 50)}");The prefixes are 0, 20, 50, 20, 50. A difference of 50 comes from 50 − 0 (days 0–1) and 50 − 0 (days 0–3, because 20 + 30 − 30 + 30 = 50). The prefix 20 would need a later prefix 70, and there is none.
Exercise
The warehouse, day by day
Solve the task in your own editor.
Delivered, sold, in stock. The warehouse starts with 40 units of one product. For ten days you have two arrays:
- delivered: 0, 25, 0, 30, 10, 0, 40, 0, 15, 0;
- sold: 12, 8, 15, 9, 20, 11, 7, 18, 10, 14.
Build the prefixes of both arrays. For the periods days 2–5, 0–9 and 6–8 print how much was delivered, how much was sold, and the stock at the end of the last day. Every answer must come from a few reads of the prefixes, without a loop over the days.
days 2–5: delivered 40, sold 55, stock at the end 30days 0–9: delivered 120, sold 124, stock at the end 36days 6–8: delivered 55, sold 35, stock at the end 50More tasks
- The average order in a period. Besides the revenue you also have the number of orders per day. An average order value does not add up, but the two sums do. How do you answer “the average order from day a to day b”?
- The best week. With the prefixes, find the seven consecutive days with the highest revenue. How many operations does that take for 365 days?
- The heat map. Add Friday and Saturday to the two-dimensional sums and find the two-hour window with the most orders in the whole week.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Best, average and worst case · Space complexity · Linear search · Hash table
- Continues in: A hash table as a counter
- Tasks on this topic: Problem-solving techniques