Problem Walkthrough

Range Sum Query 2D Mutable LeetCode 308

Solve LeetCode 308 Range Sum Query 2D Mutable using a 2D Binary Indexed Tree (Fenwick Tree) that supports O(log m * log n) point updates and rectangular region sum queries on a mutable matrix, eliminating the O(m*n) rebuild cost of naive prefix sums.

10 min read|

Range Sum Query 2D

LeetCode 308

Problem Overview — Mutable 2D Matrix with Range Queries

LeetCode 308 Range Sum Query 2D Mutable presents a classic data structure design challenge: given an m x n integer matrix, implement a class that supports two operations — update(row, col, val) which sets the value at a specific cell, and sumRegion(row1, col1, row2, col2) which returns the sum of all elements inside the rectangle defined by the top-left corner (row1, col1) and the bottom-right corner (row2, col2). The word "mutable" is the key — values in the matrix can change, so any solution must handle repeated updates efficiently.

The naive approach recomputes the region sum from scratch on every query in O(m*n) time. A static prefix sum array answers queries in O(1) but requires O(m*n) time to rebuild after every update. Neither approach is acceptable when both operations are called many times. The challenge is finding a data structure that keeps both update and query fast — ideally polylogarithmic in both dimensions.

The constraints enforce the need for efficiency: the matrix can be up to 200 x 200, and the number of update and sumRegion calls can each reach 5000. A solution with O(m*n) per operation would perform up to 200 million basic operations — too slow. The 2D Binary Indexed Tree handles both operations in O(log m * log n), making it the standard solution for this problem.

  • m x n integer matrix where 1 <= m, n <= 200
  • −10^5 <= matrix[i][j] <= 10^5
  • At most 5000 calls to update and sumRegion combined
  • update(row, col, val) sets matrix[row][col] = val
  • sumRegion(r1, c1, r2, c2) returns sum of rectangle [r1,c1] to [r2,c2]
  • Row and column indices are 0-based in the interface but 1-based internally in the BIT

Why 2D Prefix Sums Fail — The Mutability Problem

For the immutable version of this problem (LeetCode 304), a precomputed 2D prefix sum array is optimal. The prefix sum prefix[i][j] stores the sum of all elements in the rectangle from (0,0) to (i-1, j-1). Any rectangular region sum is then computable in O(1) using the inclusion-exclusion formula: sumRegion(r1,c1,r2,c2) = prefix[r2+1][c2+1] - prefix[r1][c2+1] - prefix[r2+1][c1] + prefix[r1][c1]. This is perfect when no updates occur.

Once updates are allowed, maintaining the prefix sum array becomes expensive. Changing a single cell matrix[r][c] invalidates all prefix entries prefix[i][j] where i > r and j > c — in the worst case that is the entire matrix. Rebuilding the prefix sum table takes O(m*n) time. With 5000 updates each requiring an O(m*n) rebuild, the total cost is O(5000 * m * n) = O(200 million) operations — clearly unacceptable.

What we need is a data structure that supports incremental updates: when one cell changes, only O(log m * log n) entries in the internal structure need updating, not O(m*n). This is precisely what a Binary Indexed Tree provides. The BIT's structure — where each index stores the sum of a carefully chosen range determined by the lowest set bit — enables both point updates and prefix sum queries to propagate through only O(log n) indices in each dimension.

💡

Binary Indexed Tree Balances Both Operations

A 2D Binary Indexed Tree (Fenwick Tree) achieves O(log m * log n) for both update and query. This compares favorably to O(1) query / O(m*n) update for static prefix sums, and O(m*n) query / O(1) update for raw matrix access. The BIT finds the sweet spot that makes both operations efficient simultaneously.

1D BIT Review — Fenwick Tree Fundamentals

A 1D Binary Indexed Tree is an array-based structure where each index i stores the sum of a range of original values determined by the lowest set bit of i (written lowbit(i) = i & -i). Index i covers the range (i - lowbit(i), i] — that is, lowbit(i) consecutive elements ending at position i. This arrangement allows both prefix sum queries and point updates to traverse only O(log n) indices by repeatedly adding or stripping the lowest set bit.

The update operation for a 1D BIT starts at position i and propagates to all ancestors by repeatedly adding lowbit(i): i += lowbit(i). This visits every BIT node that has i in its covered range. The prefix sum operation starts at position i and walks toward zero by repeatedly subtracting lowbit(i): i -= lowbit(i). This accumulates the sums of all non-overlapping ranges that together cover (0, i]. Both operations touch at most log2(n) indices.

The BIT uses 1-indexed arrays of size n+1, where index 0 is unused. To convert from a 0-indexed matrix interface to the 1-indexed BIT, always add 1 to row and column indices before calling BIT operations. This off-by-one translation is the single most common source of bugs when implementing a BIT from scratch.

  1. 1Initialize bit[1..n] = 0; the BIT is always 1-indexed
  2. 2lowbit(i) = i & (-i) extracts the lowest set bit of i
  3. 3Update position i by delta: while i <= n, bit[i] += delta; i += lowbit(i)
  4. 4Query prefix sum [1..i]: total = 0; while i > 0, total += bit[i]; i -= lowbit(i)
  5. 5Point update by delta: call update(i, delta) where delta = newVal - oldVal
  6. 6Range sum [l..r]: prefixSum(r) - prefixSum(l-1)

Extending to 2D BIT — A BIT of BITs

A 2D Binary Indexed Tree extends the 1D structure by nesting two BIT traversals. The internal tree array is a 2D array bit[m+1][n+1] initialized to zero. For every row index traversal (outer BIT loop), an independent column BIT traversal is performed (inner BIT loop). Each entry bit[i][j] stores the partial sum for the rectangle (i - lowbit(i), i] x (j - lowbit(j), j] in the original matrix.

The 2D update for position (r, c) with delta propagates through the outer row BIT starting at r: for each row index ri (ri starts at r, ri += lowbit(ri)), run the inner column BIT starting at c: for each column index ci (ci starts at c, ci += lowbit(ci)), add delta to bit[ri][ci]. This touches O(log m) row indices and O(log n) column indices at each row, for O(log m * log n) total operations.

The 2D prefix sum for rectangle (0,0) to (r,c) mirrors the update in reverse: walk the outer row BIT downward from r (ri -= lowbit(ri)) and for each ri walk the inner column BIT downward from c (ci -= lowbit(ci)), accumulating bit[ri][ci]. The result is the sum of all elements in the rectangle (0,0) to (r,c) in the original matrix — the 2D analog of the 1D prefix sum.

ℹ️

The 2D BIT Is a BIT of BITs

Conceptually, a 2D BIT is a BIT over rows where each node is itself a BIT over columns. The outer loop manages row-level aggregation using the same lowbit traversal as a 1D BIT, and the inner loop manages column-level aggregation identically. This nesting is why the time complexity multiplies: O(log m) for rows times O(log n) for columns gives O(log m * log n) per operation.

Region Sum with Inclusion-Exclusion

The sumRegion(r1, c1, r2, c2) operation on the 2D BIT uses the same inclusion-exclusion formula as 2D prefix sums. Define prefixSum(r, c) as the sum of all elements in the rectangle from (0,0) to (r,c) computed using the 2D BIT query. Then the rectangle sum from (r1,c1) to (r2,c2) equals prefixSum(r2,c2) - prefixSum(r1-1,c2) - prefixSum(r2,c1-1) + prefixSum(r1-1,c1-1).

The subtraction removes the regions above and to the left of the target rectangle, and the addition restores the corner region that was subtracted twice. This is exactly the 2D inclusion-exclusion principle — the same formula works whether the prefix sums come from a static table or a BIT query. When r1 = 0 or c1 = 0, the corresponding prefix sum terms are zero and the formula simplifies, but the general formula handles all edge cases correctly.

Each call to sumRegion makes at most four calls to prefixSum, each costing O(log m * log n). The total cost per sumRegion is therefore O(log m * log n). With both update and sumRegion at the same asymptotic cost, the 2D BIT achieves the optimal balance between the two operations for this problem.

  1. 1Define prefixSum(r, c): 2D BIT query for rectangle (0,0) to (r,c)
  2. 2sumRegion(r1,c1,r2,c2) = prefixSum(r2,c2)
  3. 3 minus prefixSum(r1-1, c2) [remove rows above r1]
  4. 4 minus prefixSum(r2, c1-1) [remove columns left of c1]
  5. 5 plus prefixSum(r1-1, c1-1) [add back doubly-removed corner]
  6. 6Handle r1=0 or c1=0: those prefixSum calls return 0 automatically if BIT is 1-indexed

Code Walkthrough — Python and Java

In Python, the NumericMatrix class stores the original matrix (for computing delta on update), the BIT array bit of size (m+1) x (n+1), and m, n dimensions. The __init__ method initializes bit to all zeros then calls update for every (i, j) cell with val = matrix[i][j]. The update method computes delta = val - self.matrix[r][c], updates self.matrix[r][c] = val, then runs the nested BIT loops. The query method runs the nested downward BIT loops to return the prefix sum. sumRegion applies the four-term inclusion-exclusion formula.

In Java, the NumMatrix class mirrors the Python implementation with int[][] bit and int[][] matrix. Java's lack of default-zero-initialized arrays requires explicit initialization in the constructor. The update and query methods use while loops identical in structure to the Python version. Pay careful attention to the 1-indexed BIT convention: all calls to update and query add 1 to the 0-indexed row and column arguments received from the public interface.

Both implementations store O(m*n) space for the BIT and O(m*n) for the original matrix copy, giving O(m*n) total space. The constructor initializes in O(m*n*log m*log n) time by calling update for every cell. Each subsequent update or sumRegion call runs in O(log m * log n). For the given constraints (200x200 matrix, 5000 calls), the constructor does at most 200*200*8*8 ≈ 5 million operations and each query does at most 64 operations — well within time limits.

⚠️

BIT Is 1-Indexed — Off-by-One Is the Most Common Bug

The BIT array is always 1-indexed: bit[0] is unused and the array is allocated with size m+1 by n+1. The matrix interface is 0-indexed. Every call into the BIT must add 1 to row and column indices. Forgetting this shift causes incorrect sums or index-out-of-bounds errors. Similarly, the delta in update must be newVal minus oldVal — pass the difference, not the new absolute value.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now