Loading learning content...
Imagine you're building a financial dashboard that must answer thousands of queries per second: "What was the total revenue between March 15th and June 22nd?" or "How much did we spend in Q3?" Each query asks for the sum of values within a range of your data.
The naive approach—iterating through the range and adding up values for each query—works fine for occasional questions. But at scale, with millions of data points and thousands of concurrent queries, this approach becomes a performance bottleneck that can bring your system to its knees.
Enter prefix sums, one of the most elegant and universally applicable array patterns in computer science. This technique transforms O(n) range sum queries into O(1) operations through the strategic investment of upfront computation. It's a masterclass in the fundamental algorithmic principle of trading preprocessing time for query efficiency.
By the end of this page, you will deeply understand prefix sums: their mathematical foundation, construction algorithms, constant-time range queries, space-time tradeoffs, variations (difference arrays, 2D prefix sums), and practical applications from competitive programming to production systems.
Let's formalize the problem that prefix sums solve. Given an array of n elements, we want to efficiently answer multiple queries of the form:
Range Sum Query: "What is the sum of elements from index i to index j (inclusive)?"
This seemingly simple question appears constantly in real-world applications:
The Naive Approach and Its Limitations
The straightforward solution iterates through the specified range and accumulates the sum:
123456789101112131415
// Naive range sum: O(n) per queryfunction rangeSum(arr: number[], left: number, right: number): number { let sum = 0; for (let i = left; i <= right; i++) { sum += arr[i]; } return sum;} // Example usageconst data = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]; // Query: Sum from index 2 to 7 (inclusive)const result = rangeSum(data, 2, 7);console.log(result); // 4 + 1 + 5 + 9 + 2 + 6 = 27Complexity Analysis of the Naive Approach:
This might seem acceptable, but consider the scale implications:
| Array Size (n) | Queries (Q) | Operations | At 1B ops/sec |
|---|---|---|---|
| 1,000 | 1,000 | 1 million | 1 millisecond |
| 100,000 | 10,000 | 1 billion | 1 second |
| 1,000,000 | 100,000 | 100 billion | 100 seconds |
| 10,000,000 | 1,000,000 | 10 trillion | ~3 hours |
What works in development fails in production. A million queries against a million-element array takes over an hour with the naive approach. Interactive systems become unresponsive. Batch jobs miss their windows. This is precisely where prefix sums shine.
The breakthrough behind prefix sums comes from a simple mathematical observation. Consider what happens when we compute cumulative sums from the beginning of an array:
For an array A = [a₀, a₁, a₂, a₃, ..., aₙ₋₁], we define the prefix sum array P where:
The key insight is this: any contiguous range sum can be expressed as the difference of two prefix sums.
Sum(i to j) = P[j+1] - P[i]
This works because P[j+1] contains the sum from index 0 to j, and P[i] contains the sum from index 0 to i-1. Subtracting cancels all elements before index i, leaving exactly the sum from i to j.
Visual Proof:
Let's trace through this with a concrete example:
Original array A: [3, 1, 4, 1, 5, 9, 2, 6]
Indices: 0 1 2 3 4 5 6 7
Prefix sum array P: [0, 3, 4, 8, 9, 14, 23, 25, 31]
Indices: 0 1 2 3 4 5 6 7 8
Query: Sum from index 2 to 5 (inclusive)
Why does this work?
P[6] = A[0] + A[1] + A[2] + A[3] + A[4] + A[5] = 3 + 1 + 4 + 1 + 5 + 9 = 23
P[2] = A[0] + A[1] = 3 + 1 = 4
P[6] - P[2] = (A[0] + A[1] + A[2] + A[3] + A[4] + A[5]) - (A[0] + A[1])
= A[2] + A[3] + A[4] + A[5]
= 19
The subtraction telescopes, canceling all terms before our desired range. This is the elegant heart of the prefix sum technique.
We use P of size n+1 with P[0] = 0 as a sentinel. This handles edge cases elegantly: querying from index 0 just becomes P[right+1] - P[0] = P[right+1] - 0 = P[right+1]. No special-case logic needed.
Constructing the prefix sum array is a single O(n) pass. Each element is simply the sum of the previous prefix sum and the current original element. This recurrence relation makes the algorithm both simple and efficient:
123456789101112131415161718192021222324252627282930
/** * Build a prefix sum array from the original array. * * Time Complexity: O(n) — single pass through the array * Space Complexity: O(n) — new array of size n+1 * * @param arr The original array of numbers * @returns Prefix sum array where prefix[i] = sum of arr[0..i-1] */function buildPrefixSum(arr: number[]): number[] { const n = arr.length; const prefix = new Array(n + 1).fill(0); // prefix[0] = 0 (sentinel for empty range) for (let i = 0; i < n; i++) { prefix[i + 1] = prefix[i] + arr[i]; } return prefix;} // Exampleconst data = [3, 1, 4, 1, 5, 9, 2, 6];const prefixSums = buildPrefixSum(data); console.log("Original:", data);// [3, 1, 4, 1, 5, 9, 2, 6] console.log("Prefix Sums:", prefixSums);// [0, 3, 4, 8, 9, 14, 23, 25, 31]Understanding the Recurrence:
The construction follows a simple recurrence relation:
P[0] = 0
P[i] = P[i-1] + A[i-1] for i = 1, 2, ..., n
This is equivalent to:
P[i] = Σ(k=0 to i-1) A[k]
Each prefix sum builds upon the previous one, adding exactly one new element. This incremental construction is key to the O(n) time complexity.
If you don't need the original array afterward, you can compute prefix sums in-place, using no extra space. Simply overwrite each element with its running sum. However, this destroys the original data, so use this optimization only when appropriate.
12345678910111213141516171819202122
/** * In-place prefix sum transformation. * Modifies the array to contain cumulative sums. * * After transformation: arr[i] = sum of original arr[0..i] * * Time: O(n), Space: O(1) auxiliary */function prefixSumInPlace(arr: number[]): void { for (let i = 1; i < arr.length; i++) { arr[i] += arr[i - 1]; }} // Exampleconst data = [3, 1, 4, 1, 5, 9, 2, 6];prefixSumInPlace(data);console.log(data);// [3, 4, 8, 9, 14, 23, 25, 31] // Note: Range query adjustment needed since no sentinel at index 0// Sum(i to j) = arr[j] - arr[i-1] (handle i=0 specially)With the prefix sum array constructed, any range sum query becomes a simple subtraction—two array accesses and one arithmetic operation:
12345678910111213141516171819202122
/** * Answer a range sum query in O(1) time using prefix sums. * * @param prefix The precomputed prefix sum array (size n+1) * @param left Starting index of the range (inclusive) * @param right Ending index of the range (inclusive) * @returns Sum of elements from index left to right */function rangeSum(prefix: number[], left: number, right: number): number { return prefix[right + 1] - prefix[left];} // Complete exampleconst data = [3, 1, 4, 1, 5, 9, 2, 6];const prefix = buildPrefixSum(data); // Various range queries — all O(1)console.log(rangeSum(prefix, 0, 7)); // Entire array: 31console.log(rangeSum(prefix, 2, 5)); // Indices 2-5: 19console.log(rangeSum(prefix, 0, 0)); // Single element: 3console.log(rangeSum(prefix, 4, 7)); // Indices 4-7: 22console.log(rangeSum(prefix, 3, 3)); // Single element: 1Complexity Comparison:
| Metric | Naive Approach | Prefix Sum Approach |
|---|---|---|
| Preprocessing Time | O(1) (none) | O(n) |
| Preprocessing Space | O(1) | O(n) |
| Query Time | O(n) worst case | O(1) |
| Total for Q Queries | O(Q × n) | O(n + Q) |
| Update Original Array | O(1) | O(n) to rebuild |
Prefix sums become advantageous when Q > 1 (more than one query). The break-even point is: O(n + Q) < O(Q × n), which simplifies to Q > n/(n-1) ≈ 1 for large n. In practice, even 2-3 queries justify the preprocessing for large arrays.
Handling Edge Cases:
Robust prefix sum implementations must handle several edge cases:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
/** * A complete, production-ready prefix sum implementation * with proper validation and error handling. */class PrefixSumArray { private prefix: number[]; private n: number; constructor(arr: number[]) { this.n = arr.length; this.prefix = new Array(this.n + 1).fill(0); for (let i = 0; i < this.n; i++) { this.prefix[i + 1] = this.prefix[i] + arr[i]; } } /** * Query the sum of elements in range [left, right] inclusive. * @throws Error if indices are out of bounds or invalid */ rangeSum(left: number, right: number): number { // Validate indices if (left < 0 || right >= this.n) { throw new Error(`Index out of bounds: [0, ${this.n - 1}]`); } if (left > right) { throw new Error(`Invalid range: left (${left}) > right (${right})`); } return this.prefix[right + 1] - this.prefix[left]; } /** * Get the total sum of all elements. */ totalSum(): number { return this.prefix[this.n]; } /** * Get the sum of the first k elements. */ sumFirst(k: number): number { if (k < 0 || k > this.n) { throw new Error(`k must be in range [0, ${this.n}]`); } return this.prefix[k]; }}Prefix sums exemplify a fundamental principle in algorithm design: trading space for time (or equivalently, trading preprocessing for query efficiency).
This tradeoff appears throughout computer science:
Prefix sums represent perhaps the simplest and cleanest example of this tradeoff in action.
When Prefix Sums Excel:
When to Reconsider:
When you need both efficient queries AND efficient updates, prefix sums alone aren't enough. Fenwick Trees (Binary Indexed Trees) offer O(log n) for both operations. Segment Trees offer the same complexity with more flexibility for non-associative operations. These are covered in later modules.
Prefix sums have a powerful dual: the difference array. While prefix sums accelerate range queries, difference arrays accelerate range updates.
The Problem:
Given an array, perform multiple operations of the form:
Then, reconstruct the final array.
With the naive approach, each range update takes O(k) time where k is the range size. For Q updates: O(Q × n) total.
The Difference Array Solution:
For array A, define difference array D where:
The key insight: A range update [i, j] by value v becomes just two point updates in D:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
/** * Difference Array for efficient range updates. * * The difference array D encodes the "jumps" between consecutive elements. * A range update becomes two point operations. * The final array is recovered by computing prefix sums of D. */class DifferenceArray { private diff: number[]; private n: number; constructor(arr: number[]) { this.n = arr.length; this.diff = new Array(this.n + 1).fill(0); // Build difference array this.diff[0] = arr[0]; for (let i = 1; i < this.n; i++) { this.diff[i] = arr[i] - arr[i - 1]; } } /** * Add value to all elements in range [left, right] inclusive. * Time: O(1) */ rangeUpdate(left: number, right: number, value: number): void { this.diff[left] += value; if (right + 1 < this.n) { this.diff[right + 1] -= value; } } /** * Reconstruct the final array after all updates. * Time: O(n) — essentially computing prefix sums */ getResult(): number[] { const result = new Array(this.n); result[0] = this.diff[0]; for (let i = 1; i < this.n; i++) { result[i] = result[i - 1] + this.diff[i]; } return result; }} // Example: Apply multiple range updatesconst initial = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];const da = new DifferenceArray(initial); da.rangeUpdate(2, 5, 10); // Add 10 to indices 2-5da.rangeUpdate(3, 8, 5); // Add 5 to indices 3-8da.rangeUpdate(0, 4, -3); // Subtract 3 from indices 0-4 console.log(da.getResult());// [-3, -3, 7, 12, 12, 15, 5, 5, 5, 0]Prefix sums and difference arrays are mathematical inverses. Computing prefix sums from a difference array recovers the original array. Computing differences from a prefix sum array recovers the original array. This duality is fundamental to understanding both techniques.
| Technique | Fast Operation | Slow Operation | Primary Use |
|---|---|---|---|
| Prefix Sum | Range Query O(1) | Point Update O(n) | Many queries, few updates |
| Difference Array | Range Update O(1) | Point Query O(n) | Many updates, few queries |
The prefix sum concept extends naturally to two dimensions, creating what are known as 2D prefix sums or summed area tables (a term from image processing).
The Problem:
Given a 2D matrix of values, answer queries of the form:
The 2D Prefix Sum Construction:
For matrix M, build prefix sum matrix P where P[i][j] = sum of all elements in the rectangle from (0,0) to (i-1, j-1).
The construction uses the inclusion-exclusion principle:
P[i][j] = M[i-1][j-1] + P[i-1][j] + P[i][j-1] - P[i-1][j-1]
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
/** * 2D Prefix Sum (Summed Area Table) for O(1) rectangular region sum queries. */class PrefixSum2D { private prefix: number[][]; private rows: number; private cols: number; constructor(matrix: number[][]) { if (matrix.length === 0 || matrix[0].length === 0) { this.rows = 0; this.cols = 0; this.prefix = []; return; } this.rows = matrix.length; this.cols = matrix[0].length; // Create prefix sum matrix with sentinel row and column this.prefix = Array.from( { length: this.rows + 1 }, () => new Array(this.cols + 1).fill(0) ); // Build using inclusion-exclusion for (let i = 1; i <= this.rows; i++) { for (let j = 1; j <= this.cols; j++) { this.prefix[i][j] = matrix[i - 1][j - 1] + this.prefix[i - 1][j] + this.prefix[i][j - 1] - this.prefix[i - 1][j - 1]; } } } /** * Query sum of rectangular region from (r1,c1) to (r2,c2) inclusive. * Uses inclusion-exclusion to compute in O(1). */ regionSum(r1: number, c1: number, r2: number, c2: number): number { // Convert to 1-indexed prefix array coordinates r1++; c1++; r2++; c2++; return this.prefix[r2][c2] - this.prefix[r1 - 1][c2] - this.prefix[r2][c1 - 1] + this.prefix[r1 - 1][c1 - 1]; }} // Exampleconst matrix = [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]; const ps2d = new PrefixSum2D(matrix); // Sum of entire matrixconsole.log(ps2d.regionSum(0, 0, 2, 3)); // 78 // Sum of middle 2x2 region (rows 0-1, cols 1-2)console.log(ps2d.regionSum(0, 1, 1, 2)); // 2+3+6+7 = 18Summed area tables were introduced in computer graphics for fast texture mapping. They're used extensively in image processing for features like box blur, where you need to average pixels in a rectangular region. The Viola-Jones face detection algorithm famously uses integral images (another name for 2D prefix sums) to compute Haar-like features in constant time.
Complexity Analysis:
Prefix sums unlock efficient solutions to a wide variety of problems. Here are several classics that every serious programmer should know:
1234567891011121314151617181920212223242526272829
/** * Count subarrays with sum equal to k. * Uses prefix sums + hash map for O(n) solution. */function subarraySum(nums: number[], k: number): number { const prefixCount = new Map<number, number>(); prefixCount.set(0, 1); // Empty prefix has sum 0 let count = 0; let currentSum = 0; for (const num of nums) { currentSum += num; // How many previous prefix sums equal (currentSum - k)? const target = currentSum - k; if (prefixCount.has(target)) { count += prefixCount.get(target)!; } // Record this prefix sum prefixCount.set(currentSum, (prefixCount.get(currentSum) || 0) + 1); } return count;} console.log(subarraySum([1, 1, 1], 2)); // 2console.log(subarraySum([1, 2, 3], 3)); // 2When you see 'sum of contiguous elements' or 'range query' in a problem, prefix sums should immediately come to mind. The technique is so fundamental that recognizing its applicability becomes second nature with practice.
Prefix sums represent one of the most important array patterns in computer science—simple to implement, mathematically elegant, and universally applicable. Let's consolidate the key insights:
What's Next:
With prefix sums mastered, you have a powerful tool for aggregate computations. In the next page, we explore Kadane's Algorithm—an elegant technique for finding the maximum sum subarray that builds on the intuition developed here but takes a fundamentally different approach through dynamic programming.
Prefix sums are now part of your algorithmic toolkit. You'll find them appearing again and again—in competitive programming, system design, and production code. The technique is deceptively simple but remarkably powerful. Master it, and you'll solve entire categories of problems with ease.