Skip to main content

Mastering 2D Prefix Sums for Range Query Optimization

Learn how to implement 2D prefix sums for efficient range sum queries with O(1) query time after O(mn) preprocessing.

2D prefix sums provide an elegant solution for answering range sum queries efficiently by precomputing cumulative values across a matrix, enabling constant-time range sum calculations after linear preprocessing.

1. Pattern Restatement

Clarifying questions to ask the interviewer:

  1. Are we guaranteed that the input grid will have at least one row and column, or should we handle empty inputs?
  2. Do we need to support multiple range sum queries on the same grid, or is this a one-time operation?
  3. Can we modify the input grid in-place to store prefix sums, or must we preserve the original data?
  4. What is the expected time complexity for query operations? Should we aim for O(1) per query?
  5. Are there any constraints on the grid size (e.g., maximum rows/columns), or should we consider scalability?

Restate the pattern or problem type in your own words: The 2D prefix sum pattern is a preprocessing technique used to efficiently compute range sums over rectangular subregions of a 2D grid. Instead of calculating the sum of elements in each query region from scratch, we build a prefix sum matrix where each cell (i,j) contains the sum of all elements from the top-left corner (0,0) to position (i,j). This allows us to answer range sum queries in constant time after O(mn) preprocessing.

Clarify the signal words and constraint shape that point to it: Key signals include "range sum queries", "2D grid", "multiple sum operations on subregions", and "efficiency requirements". The constraint shape typically involves a static grid with frequent sum queries over rectangular regions, where preprocessing costs are acceptable if query times can be optimized.

Note any assumptions you are making:

  • The input grid is static (no updates after preprocessing)
  • Grid elements are numeric (integers or floats)
  • Range queries are rectangular subregions
  • We can afford O(mn) preprocessing time for O(1) query time

Identify whether the pattern is best suited for static, streaming, or online scenarios if relevant: This pattern is best suited for static scenarios where the grid remains fixed and multiple range sum queries need to be answered efficiently. It's not ideal for streaming data where the grid changes frequently, as the prefix sum array would need to be recomputed each time. For online scenarios with updates, we'd want a more dynamic structure like a 2D segment tree or Fenwick tree.

2. When to Use This Pattern

Signal words and constraint shapes in a problem statement that indicate this pattern:

  • "Range sum queries" or "rectangle sum queries"
  • "Multiple queries on a 2D matrix"
  • "Update and query operations" (for dynamic variants)
  • "Preprocessing allowed" or "offline queries"
  • "Sum of elements in a submatrix"

Why this pattern fits better than the most common alternatives: The 2D prefix sum pattern is optimal for static 2D range sum queries because it reduces query time from O(n²) to O(1) after O(mn) preprocessing. Compared to brute force approaches, it's significantly faster for multiple queries. Unlike segment trees or Fenwick trees (which are better for dynamic updates), 2D prefix sums shine when the matrix is static and we're only doing range sum queries.

Conditions where a variation or related pattern applies instead:

  • For dynamic updates to the matrix, use 2D Binary Indexed Trees (Fenwick trees) or segment trees
  • For streaming data where we only care about recent updates, use sliding window techniques
  • For sparse matrices with mostly zero values, consider sparse matrix representations

The core insight that makes this pattern work: The key insight is that we can precompute a prefix sum matrix where each cell (i,j) stores the sum of all elements from (0,0) to (i,j). Then any rectangular range sum query can be computed using inclusion-exclusion principle: sum = prefix[i2][j2] - prefix[i1-1][j2] - prefix[i2][j1-1] + prefix[i1-1][j1-1]. This works because we're counting areas multiple times in the subtraction process.

This pattern is ideal for problems where:

  1. The input matrix is static (no updates)
  2. Multiple range sum queries are expected
  3. Query time efficiency is more important than preprocessing time
  4. We're dealing with rectangular regions rather than arbitrary shapes

3. Approach Evolution

Interview communication script:

My first instinct is to use a brute force approach where for each query, I compute the sum by iterating through the submatrix. But that's O(n²) per query because we have to sum up all elements in the rectangle. If there are Q queries, that becomes O(Q×n²) which is inefficient.

Let me think about how to optimize this. I notice that if I precompute prefix sums for the entire 2D matrix, I can reduce each query to O(1) time by using inclusion-exclusion principle. The key insight is that once I build the prefix sum structure, any rectangular range query can be computed by combining four precomputed values.

The optimization comes from the fact that we're not recomputing the same sums repeatedly. Instead of computing the sum for each query from scratch, we build a structure that allows us to decompose any rectangular query into four prefix sums using the inclusion-exclusion principle.

Let me walk through this approach more carefully:

First, I'll build a 2D prefix sum array where prefix[i][j] represents the sum of all elements from (0,0) to (i,j). Then for any query rectangle from (r1,c1) to (r2,c2), I can compute the sum using: sum = prefix[r2][c2] - prefix[r1-1][c2] - prefix[r2][c1-1] + prefix[r1-1][c1-1]

This approach gives us O(1) query time after O(m×n) preprocessing.

Brute-force approach and why it is not ideal:

The brute force approach would iterate through every element in the specified rectangle for each query. For a single query, this takes O((r2-r1+1) × (c2-c1+1)) time. If we have Q queries on an m×n matrix, the total time complexity becomes O(Q × m × n), which is inefficient for multiple queries.

Improved approach and the key optimization:

The key optimization is to precompute 2D prefix sums. Instead of recomputing sums for each query, we build a structure where:

  • prefix[i][j] = sum of all elements from (0,0) to (i,j)
  • For any query rectangle (r1,c1) to (r2,c2), we use inclusion-exclusion principle: sum = prefix[r2][c2] - prefix[r1-1][c2] - prefix[r2][c1-1] + prefix[r1-1][c1-1]

This reduces query time from O(m×n) to O(1).

Final approach with step-by-step reasoning:

  1. Build 2D prefix sum array where prefix[i][j] = sum of all elements from (0,0) to (i,j)
  2. For each query rectangle (r1,c1) to (r2,c2):
    • Use inclusion-exclusion principle to compute sum in O(1) time
    • Handle boundary conditions where r1-1 or c1-1 might be negative

Important invariants that must hold:

  • The prefix sum array must be built correctly to ensure inclusion-exclusion works
  • All indices used in the query must be valid (not out of bounds)
  • The prefix array should handle edge cases where indices are negative (treat as 0)

4. Data Structure Choice

For 2D prefix sums, the primary data structure is a 2D array that stores cumulative sums. This array is typically one size larger than the original input matrix to simplify boundary handling.

Primary Data Structures Used

2D Prefix Sum Array:

  • Size: (rows + 1) × (cols + 1)
  • Purpose: Stores cumulative sums where prefix[i][j] represents the sum of all elements in the submatrix from (0,0) to (i-1,j-1) in the original matrix
  • Why needed: Enables O(1) range sum queries by precomputing cumulative values

Design Rationale

The key insight is that we can compute prefix sums in a single pass through the original matrix, building up cumulative values from top-left to bottom-right. By using an extended array (with extra row/column), we avoid boundary checks during the prefix sum computation.

Space Trade-off

The space complexity is O(m×n) where m and n are the dimensions of the input matrix. This is necessary to store all prefix sums for efficient range queries. The extra row and column simplify the mathematical relationships but increase space usage by a constant factor.

Helper Structures

No additional helper structures are needed beyond the prefix sum array itself. The computation can be done in-place on the extended array, making it memory-efficient for the preprocessing step.

Why This Choice Works

This approach provides optimal query performance (O(1) range sum queries) at the cost of O(m×n) preprocessing time and space. It's the standard solution for static 2D range sum problems where multiple queries are expected, as it transforms each query from O(m×n) to O(1).

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to show how it works step by step.

Example: Given a 2D matrix [[1,2,3],[4,5,6],[7,8,9]] and queries [[0,0,1,1],[1,1,2,2]] asking for sum of submatrices.

Step 1: Build the prefix sum matrix

  • Original matrix:
1 2 3
4 5 6
7 8 9
  • Prefix sum matrix (after preprocessing):
1  3  6
5  12 21
12 27 45

Step 2: Process first query [0,0,1,1]

  • This asks for sum of submatrix from (0,0) to (1,1)
  • Using formula: prefix[1][1] - prefix[-1][1] - prefix[1][-1] + prefix[-1][-1]
  • Which becomes: 12 - 0 - 0 + 0 = 12
  • This correctly gives us sum of elements: 1+2+4+5 = 12

Step 3: Process second query [1,1,2,2]

  • This asks for sum of submatrix from (1,1) to (2,2)
  • Using formula: prefix[2][2] - prefix[0][2] - prefix[2][0] + prefix[0][0]
  • Which becomes: 45 - 21 - 12 + 1 = 13
  • This correctly gives us sum of elements: 5+6+8+9 = 28

Key insight: The prefix sum matrix allows us to compute any rectangular submatrix sum in O(1) time by using the inclusion-exclusion principle with four precomputed prefix sums.

Edge case handling: When coordinates are at boundaries (like negative indices), we treat them as 0, which correctly handles queries starting from row/column 0.

public class RangeSumQuery2D {
    private int[][] prefix;
    
    public RangeSumQuery2D(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return;
        }
        
        int rows = matrix.length;
        int cols = matrix[0].length;
        prefix = new int[rows + 1][cols + 1];
        
        // Build prefix sum matrix
        for (int i = 1; i <= rows; i++) {
            for (int j = 1; j <= cols; j++) {
                prefix[i][j] = matrix[i-1][j-1] 
                    + prefix[i-1][j] 
                    + prefix[i][j-1] 
                    - prefix[i-1][j-1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return prefix[row2+1][col2+1] 
            - prefix[row1][col2+1] 
            - prefix[row2+1][col1] 
            + prefix[row1][col1];
    }
}

6. Java Solution

public class RangeSumQuery2D {
    private int[][] prefixSum;
    
    public RangeSumQuery2D(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return;
        }
        
        int rows = matrix.length;
        int cols = matrix[0].length;
        
        // Initialize prefix sum array with one extra row and column for easier calculation
        prefixSum = new int[rows + 1][cols + 1];
        
        // Build the prefix sum matrix
        for (int i = 1; i <= rows; i++) {
            for (int j = 1; j <= cols; j++) {
                prefixSum[i][j] = matrix[i-1][j-1] 
                                + prefixSum[i-1][j] 
                                + prefixSum[i][j-1] 
                                - prefixSum[i-1][j-1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        // Handle invalid ranges
        if (row1 < 0 || col1 < 0 || row2 >= prefixSum.length - 1 || 
            col2 >= prefixSum[0].length - 1 || row1 > row2 || col1 > col2) {
            return 0;
        }
        
        // Calculate sum using inclusion-exclusion principle
        return prefixSum[row2 + 1][col2 + 1] 
             - prefixSum[row1][col2 + 1] 
             - prefixSum[row2 + 1][col1] 
             + prefixSum[row1][col1];
    }
}

This solution implements a 2D range sum query system using prefix sums. The constructor builds a prefix sum matrix where each cell (i,j) contains the sum of all elements from (0,0) to (i-1,j-1) in the original matrix. The sumRegion method then uses the inclusion-exclusion principle to quickly compute the sum of any rectangular region in O(1) time by combining four precomputed prefix sums.

7. Complexity Analysis

Time Complexity: O(1) per query, O(mn) preprocessing

The preprocessing phase builds the 2D prefix sum array by iterating through each cell once and computing prefix[i][j] = matrix[i][j] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1]. This requires exactly m × n operations where m and n are the dimensions of the input matrix.

Each query operation computes the sum of a submatrix in constant time by accessing four precomputed values from the prefix array and applying the inclusion-exclusion principle: sum = prefix[row2][col2] - prefix[row1-1][col2] - prefix[row2][col1-1] + prefix[row1-1][col1-1].

Space Complexity: O(mn) for the prefix array

The space complexity is dominated by the additional 2D prefix sum array that stores precomputed cumulative sums. The original matrix is not modified in-place, so we do not count its space in the complexity analysis. We use an additional O(mn) space for the prefix array, which is necessary to achieve O(1) query time.

Why these bounds are correct:

The preprocessing is linear in the total number of elements, and each query performs a fixed number of arithmetic operations regardless of the submatrix size. The bounds are optimal for this problem because:

  1. We must read all input elements at least once to build the prefix structure
  2. Query performance cannot be better than O(1) since we must return a computed sum value
  3. The prefix array is the minimal space needed to support constant-time range queries

This solution achieves optimal time complexity for the static 2D range sum problem where preprocessing is allowed. The O(1) query time is optimal for the static case, and the O(mn) preprocessing is necessary to achieve this query performance.

The solution is near-optimal for the static 2D range sum problem where we're allowed to preprocess the input data. Any algorithm that supports O(1) query time for 2D range sums must use at least O(mn) space, and any algorithm that builds the prefix structure must process each element at least once.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty grid: Handle gracefully when rows = 0 or cols = 0, though prefix sum arrays will be empty
  • Single cell: When grid has only one element (1×1), prefix sum is just that element
  • Single row or column: Prefix sums for single-row grids are straightforward but must handle bounds correctly

Duplicate Values and Repeated States

  • All zeros: Prefix sums will be all zeros, but algorithm still works correctly
  • Identical elements: No special handling needed, though repeated values may cause confusion in debugging
  • Negative numbers: Prefix sums can be negative, which is valid but requires careful handling in queries

Boundary Conditions

  • Query bounds: Ensure that query rectangles don't exceed grid dimensions (0 ≤ row1 ≤ row2 < rows, 0 ≤ col1 ≤ col2 < cols)
  • Index bounds: Handle invalid indices gracefully, particularly when querying rectangles that extend beyond grid dimensions
  • Array indexing: Java arrays are 0-indexed, so ensure all boundary checks properly account for this

Common Implementation Mistakes

  • Off-by-one errors in prefix sum construction: When building 2D prefix sums, ensure prefix[i][j] = grid[i][j] + prefix[i-1][j] + prefix[i][j-1] - prefix[i-1][j-1] includes all necessary terms
  • Incorrect indexing in query: When computing rectangle sums, ensure prefix[row2+1][col2+1] - prefix[row1][col2+1] - prefix[row2+1][col1] + prefix[row1][col1] uses correct indices
  • Not handling zero-based indexing: Confusing 0-based array indices with 1-based coordinate systems
  • Memory allocation issues: Not allocating proper size for prefix sum arrays (should be (rows+1) × (cols+1) to avoid boundary checks)
  • Missing base case handling: For 0×0 grids or queries that don't exist, ensure proper error handling or return values

Additional Considerations

  • Large grids: For very large grids, consider memory constraints when building prefix sums (O(mn) space)
  • Immutable input: If grid cannot be mutated, ensure prefix sum construction doesn't modify original data
  • Integer overflow: For large grids with large values, consider using long for prefix sums to prevent overflow in sum calculations

9. Alternative Approaches

There are several alternative approaches to solving problems that might seem like they could use 2D prefix sums, each with different trade-offs:

Brute Force Range Queries: For each query, iterate through the entire submatrix to compute sums. This approach has O(n²) time per query and is only acceptable for very few queries or small matrices. It's simple to implement but inefficient for multiple queries.

Segment Trees with 2D Range Queries: For dynamic updates and queries, a 2D segment tree can be used. This approach has O(log²n) time complexity for both updates and queries but requires significantly more complex implementation with higher constant factors. It's useful when the matrix is frequently updated.

Square Root Decomposition: Divide the 2D matrix into blocks and precompute sums for each block. This can achieve O(√n) query time with O(n²) preprocessing. It's a middle ground between brute force and full 2D prefix sums but is more complex to implement correctly.

Sparse Matrix Techniques: If the matrix has many zero elements, storing only non-zero values with their coordinates can be more efficient. This approach is beneficial when the matrix is sparse but doesn't directly address the range sum query problem.

The chosen 2D prefix sum approach is optimal for static matrices with multiple range sum queries because it provides O(1) query time after O(n²) preprocessing, which is the best possible for this problem type. The alternatives either have worse query complexity or are more complex to implement without significant benefits in typical use cases.

10. Test Cases

// Test case 1: Basic functionality with a 3x3 matrix
int[][] matrix1 = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
// Query: sumRegion(0,0,2,2) should return 45
// Query: sumRegion(1,1,2,2) should return 28

// Test case 2: Single cell query
int[][] matrix2 = {{5}};
// Query: sumRegion(0,0,0,0) should return 5

// Test case 3: Empty matrix (edge case)
int[][] matrix3 = {};
// Should handle gracefully with proper bounds checking

// Test case 4: Matrix with negative numbers
int[][] matrix4 = {
    {-1, 2, -3},
    {4, -5, 6},
    {-7, 8, -9}
};
// Query: sumRegion(0,0,2,2) should return -5

// Test case 5: Query spanning entire matrix
int[][] matrix5 = {
    {1, 2},
    {3, 4}
};
// Query: sumRegion(0,0,1,1) should return 10

The test cases cover fundamental scenarios including basic functionality, edge cases with single elements, matrices containing negative values, and boundary conditions. These tests ensure the implementation correctly handles various matrix configurations and query ranges while maintaining proper bounds checking for invalid inputs.

11. Final Summary

The core idea is to precompute 2D prefix sums to enable O(1) range sum queries by leveraging the mathematical relationship sum = prefix[r2][c2] - prefix[r1-1][c2] - prefix[r2][c1-1] + prefix[r1-1][c1-1]. This solution works by building a cumulative sum matrix where each cell contains the sum of all elements in the rectangle from (0,0) to (i,j), allowing efficient range sum calculations without reprocessing the entire submatrix. The approach is optimal for static scenarios with multiple range sum queries, trading initial O(mn) preprocessing for O(1) query time.

Was this article helpful?

Topics in this article

More from DSA Patterns

  • DSA Patterns

    Trapping Rain Water Algorithm Solution

    Complete Java implementation of the trapping rain water problem with multiple approaches including two-pointer and dynamic programming techniques.

  • DSA Problems

    Trapping Rain Water Algorithm Solution

    Comprehensive Java implementation of the trapping rain water problem with optimal time and space complexity analysis.

  • DSA Patterns

    Best Time to Buy and Sell Stock Algorithm Solutions

    Comprehensive Java solutions for the classic stock trading problem including single transaction, multiple transactions, and cooldown constraints with optimal time and space complexity analysis.

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.