304. Range Sum Query 2D Immutable - Explanation

Problem Link

Description

You are given a 2D matrix matrix, handle multiple queries of the following type:

  • Calculate the sum of the elements of matrix inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2).

Implement the NumMatrix class:

  • NumMatrix(int[][] matrix) Initializes the object with the integer matrix matrix.
  • int sumRegion(int row1, int col1, int row2, int col2) Returns the sum of the elements of matrix inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2).
    You must design an algorithm where sumRegion works on O(1) time complexity.

Example 1:

Input: ["NumMatrix", "sumRegion", "sumRegion", "sumRegion"]
[[[[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]], [2, 1, 4, 3], [1, 1, 2, 2], [1, 2, 2, 4]]

Output: [null, 8, 11, 12]

Explanation:
NumMatrix numMatrix = new NumMatrix([[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]]);
numMatrix.sumRegion(2, 1, 4, 3); // return 8 (i.e sum of the red rectangle)
numMatrix.sumRegion(1, 1, 2, 2); // return 11 (i.e sum of the green rectangle)
numMatrix.sumRegion(1, 2, 2, 4); // return 12 (i.e sum of the blue rectangle)

Constraints:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 200
  • -10,000 <= matrix[i][j] <= 10,000
  • 0 <= row1 <= row2 < m
  • 0 <= col1 <= col2 < n
  • At most 10,000 calls will be made to sumRegion.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • 2D Arrays (Matrices) - Working with row and column indices to access and iterate over rectangular regions
  • Prefix Sums - Precomputing cumulative sums for constant-time range queries
  • Inclusion-Exclusion Principle - Computing 2D region sums by combining overlapping rectangular areas

1. Brute Force

Intuition

The most straightforward approach is to iterate through every cell in the specified rectangular region and sum up all the values. For each query, we simply loop from the top-left corner to the bottom-right corner and accumulate the result. While this is easy to implement, it becomes slow when we have many queries or large regions to sum.

Algorithm

  1. Store the original matrix.
  2. For each sumRegion(row1, col1, row2, col2) query:
    • Initialize res = 0.
    • Iterate through all rows from row1 to row2.
    • For each row, iterate through all columns from col1 to col2.
    • Add each cell value to res.
  3. Return res.
class NumMatrix:

    def __init__(self, matrix: list[list[int]]):
        self.matrix = matrix

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        res = 0
        for r in range(row1, row2 + 1):
            for c in range(col1, col2 + 1):
                res += self.matrix[r][c]
        return res

Time & Space Complexity

  • Time complexity: O(mn)O(m * n) for each query.
  • Space complexity: O(1)O(1)

Where mm is the number of rows and nn is the number of columns in the matrix.


2. One Dimensional Prefix Sum

Intuition

Instead of summing every cell for each query, we can precompute prefix sums for each row. Each prefixSum[row][col] stores the sum of all elements from matrix[row][0] to matrix[row][col]. This way, finding the sum of any range within a single row takes constant time. For a rectangular region spanning multiple rows, we sum each row's contribution using its prefix sum.

Algorithm

  1. Build a 2D prefix sum array where prefixSum[row][col] holds the cumulative sum of row row from column 0 to col.
  2. For each sumRegion(row1, col1, row2, col2) query:
    • Initialize res = 0.
    • For each row from row1 to row2:
      • Add prefixSum[row][col2] to res.
      • If col1 > 0, subtract prefixSum[row][col1 - 1] from res.
  3. Return res.
class NumMatrix:

    def __init__(self, matrix: list[list[int]]):
        self.prefixSum = [[0] * len(matrix[0]) for _ in range(len(matrix))]

        for row in range(len(matrix)):
            self.prefixSum[row][0] = matrix[row][0]
            for col in range(1, len(matrix[0])):
                self.prefixSum[row][col] = self.prefixSum[row][col - 1] + matrix[row][col]

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        res = 0
        for row in range(row1, row2 + 1):
            if col1 > 0:
                res += self.prefixSum[row][col2] - self.prefixSum[row][col1 - 1]
            else:
                res += self.prefixSum[row][col2]
        return res

Time & Space Complexity

  • Time complexity: O(m)O(m)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the matrix.


3. Two Dimensional Prefix Sum

Intuition

We can extend prefix sums to two dimensions. The idea is to precompute sumMat[r][c] as the sum of all elements in the rectangle from (0, 0) to (r - 1, c - 1). To find the sum of any rectangular region, we use the inclusion-exclusion principle: take the sum up to the bottom-right corner, subtract the regions above and to the left, then add back the top-left corner (which was subtracted twice).

Algorithm

  1. Create a prefix sum matrix sumMat of size (ROWS + 1) x (COLS + 1) initialized to zero.
  2. Build the prefix sum matrix:
    • For each row r, maintain a running prefix sum across columns.
    • Set sumMat[r + 1][c + 1] = prefix + sumMat[r][c + 1].
  3. For each sumRegion(row1, col1, row2, col2) query:
    • Compute bottomRight = sumMat[row2 + 1][col2 + 1].
    • Subtract above = sumMat[row1][col2 + 1].
    • Subtract left = sumMat[row2 + 1][col1].
    • Add back topLeft = sumMat[row1][col1].
  4. Return bottomRight - above - left + topLeft.
class NumMatrix:

    def __init__(self, matrix: list[list[int]]):
        ROWS, COLS = len(matrix), len(matrix[0])
        self.sumMat = [[0] * (COLS + 1) for _ in range(ROWS + 1)]

        for r in range(ROWS):
            prefix = 0
            for c in range(COLS):
                prefix += matrix[r][c]
                above = self.sumMat[r][c + 1]
                self.sumMat[r + 1][c + 1] = prefix + above

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        row1, col1, row2, col2 = row1 + 1, col1 + 1, row2 + 1, col2 + 1
        bottomRight = self.sumMat[row2][col2]
        above = self.sumMat[row1 - 1][col2]
        left = self.sumMat[row2][col1 - 1]
        topLeft = self.sumMat[row1 - 1][col1 - 1]
        return bottomRight - above - left + topLeft

Time & Space Complexity

  • Time complexity: O(1)O(1) for each query.
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns in the matrix.


Common Pitfalls

Forgetting the Inclusion-Exclusion Principle

When computing the sum of a rectangular region using 2D prefix sums, you must subtract the regions above and to the left, then add back the top-left corner that was subtracted twice. The formula is bottomRight - above - left + topLeft. Forgetting to add back the top-left corner results in under-counting, while forgetting to subtract either the above or left region causes over-counting.

Off-by-One Errors with Prefix Sum Indices

The prefix sum matrix is typically sized (rows + 1) x (cols + 1) to handle edge cases where the query starts at row 0 or column 0. Mixing up whether indices are 0-based or 1-based leads to accessing wrong cells or out-of-bounds errors. When querying sumRegion(row1, col1, row2, col2), remember to shift indices appropriately when accessing the prefix sum matrix.

Building Prefix Sum Matrix Incorrectly

The 2D prefix sum at position (r, c) should represent the sum of all elements from (0, 0) to (r-1, c-1). A common mistake is computing prefix sums row by row without properly incorporating the contribution from previous rows. The correct formula is prefix[r+1][c+1] = matrix[r][c] + prefix[r][c+1] + prefix[r+1][c] - prefix[r][c], or equivalently, accumulate row prefix and add the cell directly above.