308. Range Sum Query 2D - Mutable
Problem Description
The problem provides a two-dimensional matrix, matrix
, and requires us to design a class, NumMatrix
, to perform two types of operations efficiently:
- Update the value at a specific cell
(row, col)
in the matrix. - Calculate the sum of elements within a rectangle specified by its upper left
(row1, col1)
and lower right(row2, col2)
corners.
To encapsulate these operations, the NumMatrix
class is required to have the following methods:
NumMatrix(int[][] matrix)
initializes the instance with the matrix.void update(int row, int col, int val)
updates the value at the matrix cell(row, col)
toval
.int sumRegion(int row1, int col1, int row2, int col2)
calculates and returns the sum of the elements within the defined rectangle.
The problem challenges the programmer to find an efficient way to perform both operations considering that there will be multiple queries of both types.
Intuition
A straightforward approach to solve this problem would involve implementing the update
function in O(1)
time by directly changing the value in the matrix and the sumRegion
function in O(R*C)
time, where R and C are, respectively, the number of rows and columns to be summed, by iterating through all the numbers in the specified rectangle. However, this naive method would be highly inefficient when dealing with a large matrix and many sumRegion
queries.
To improve efficiency, especially for the sum query operation, we can use more sophisticated data structures such as Binary Indexed Trees (BITs) or Segment Trees, which are designed for high-performance range queries. Both of these data structures can help us achieve a better-than-naive time complexity for range sum queries and updates, typically O(logN)
for update and query operations.
For this solution, a Binary Indexed Tree (also known as a Fenwick Tree) approach is taken. The concept of a BIT leverages the cumulative frequency up to a given index to compute range sums more efficiently. The BIT is a representation of the matrix that allows both updating an element and querying the prefix sum up to a point in logarithmic time. It uses a clever structure to store cumulative sums with the ability to update frequencies and query cumulative frequencies in O(logN)
time complexity.
The solution includes a BinaryIndexedTree
class for handling update and query operations on 1D arrays. The NumMatrix
class initializes a list of these trees, each representing a row in the matrix, and performs updates and queries using these trees. This allows efficient updates on single elements and fast calculation of rectangle sums by summing the range sums of multiple binary indexed trees corresponding to matrix rows.
Learn more about Segment Tree patterns.
Solution Approach
The solution uses a Binary Indexed Tree (BIT) or Fenwick Tree data structure to optimize both the update
and sumRegion
operations. A BIT arranges data in such a way that it allows for both update and query operations to be performed in O(logN)
time complexity.
Binary Indexed Tree Explained
A Binary Indexed Tree is a data structure that stores cumulative sums of an array and supports two operations efficiently:
- Update: Increments an element in the array and consequently updates the cumulative sums.
- Query: Calculates the prefix sum up to a given index, that is, the sum of all elements from the beginning of the array up to the specified index.
The key idea behind BIT is that it uses a binary representation of indices and exploits the fact that numbers can be decomposed into the sum of powers of 2. This allows for both the update and the query operations to skip over large sections of the array to achieve the desired logarithmic time complexity.
How the BIT is Implemented in the Solution
In the given solution, a BinaryIndexedTree
class defines two important functions:
-
update(int x, int delta)
: This function incrementally updates the element at indexx
bydelta
and updates all related elements in the BIT. It iteratively addsdelta
to the current indexx
and moves to the next index by using thelowbit
function, which essentially calculates the length of the interval represented by that index.def update(self, x, delta): while x <= self.n: self.c[x] += delta x += BinaryIndexedTree.lowbit(x)
-
query(int x)
: This function calculates the prefix sum up to indexx
. It works by iteratively adding the value of the current index to the sums
and moving to the previous relevant index by subtracting thelowbit
value from the current indexx
.def query(self, x): s = 0 while x > 0: s += self.c[x] x -= BinaryIndexedTree.lowbit(x) return s
Integration with the NumMatrix Class
The NumMatrix
class uses the BinaryIndexedTree
class to manage each row of the given matrix:
-
Initialization (
__init__
): Here, a list ofBinaryIndexedTree
instances is created, where each instance corresponds to a row in the matrix. Each element of the row is updated in its corresponding tree during the initialization. -
Updating a cell (
update
): To update a value in a particular cell identified by(row, col)
, the difference between the new value and the old value is found by querying the cumulative frequency up untilcol
and the frequency atcol-1
. This differenceval - prev
is then used to update the tree for that specific row.def update(self, row: int, col: int, val: int) -> None: tree = self.trees[row] prev = tree.query(col + 1) - tree.query(col) tree.update(col + 1, val - prev)
-
Calculating sum of a region (
sumRegion
): To calculate the sum within a specified region, it iterates through eachrow
fromrow1
torow2
(inclusive), querying theBinaryIndexedTree
for that row for the sum up tocol2
and subtracting the sum up tocol1-1
. Adding these together gives the sum of the rectangle.def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int: return sum( tree.query(col2 + 1) - tree.query(col1) for tree in self.trees[row1 : row2 + 1] )
By leveraging BITs, the solution maintains a balance between the time complexity of update and range sum queries, ensuring that both operations are done efficiently even when faced with a large number of queries.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's take a small matrix to walk through the example:
Matrix: 1 2 3 4
We want to initialize the NumMatrix
class, perform an update operation, and then perform a sumRegion
query to illustrate how the solution approach works.
Step 1: Initializing NumMatrix
matrix = [ [1, 2], [3, 4] ] numMatrix = NumMatrix(matrix)
Upon initialization, a BinaryIndexedTree
instance is created for each row in the matrix. The matrix is transformed into a format that the Binary Indexed Trees can work with. This means that after initialization, we have two Binary Indexed Trees corresponding to each row of the matrix.
Step 2: Updating a cell
Let's update the value of the cell at the first row and second column (indexing from 0) to 5. This is how we call the update method:
numMatrix.update(0, 1, 5)
Now, the matrix and the corresponding Binary Indexed Trees are as follows:
Matrix: 1 5 3 4 Binary Indexed Trees would internally represent cumulative sums like this: Tree for first row: [1, 6] Tree for second row: [3, 7]
Step 3: Calculate sum of a region
Suppose we want to calculate the sum of elements within the rectangle from (0, 0)
to (1, 1)
, which covers the entire matrix:
sum = numMatrix.sumRegion(0, 0, 1, 1)
The query operation would perform as follows:
- The iteration starts from
row1
, which is 0, and ends atrow2
, which is 1, inclusive. - For the first row (
row1
or 0), it calculates the sum of the range from the beginning of the row up tocol2
(1), which is6
, and subtracts the sum up tocol1
- 1 (ifcol1
is greater than 0), which in this case, isn't subtracted sincecol1
is 0. This gives us 6 for the first row. - For the second row (
row2
or 1), it does the same operation and calculates the sum across the row up tocol2
(1), which is7
, yielding a value of 7. - The sum of values obtained from all the rows within the specified range is then totaled. In this case,
6 + 7
which equals13
.
The answer is therefore 13
, which is indeed the sum of all values in the matrix after the update. By combining these steps, the NumMatrix
class can efficiently update values and calculate the sum of regions within the matrix.
Solution Implementation
1class BinaryIndexedTree:
2 def __init__(self, size):
3 self.size = size
4 self.tree = [0] * (size + 1)
5
6 @staticmethod
7 def lowbit(index):
8 # Retrieves the right-most bit that is different from zero
9 return index & -index
10
11 def update(self, index, delta):
12 # Adds 'delta' to the element at 'index' and all relevant indexes in BIT
13 while index <= self.size:
14 self.tree[index] += delta
15 index += BinaryIndexedTree.lowbit(index)
16
17 def query(self, index):
18 # Computes the prefix sum up to 'index'
19 sum = 0
20 while index > 0:
21 sum += self.tree[index]
22 index -= BinaryIndexedTree.lowbit(index)
23 return sum
24
25
26class NumMatrix:
27 def __init__(self, matrix):
28 # Initialize a list of Binary Indexed Trees, one for each row in the given matrix
29 self.trees = []
30 num_columns = len(matrix[0])
31 for row in matrix:
32 bit = BinaryIndexedTree(num_columns)
33 for col, value in enumerate(row):
34 bit.update(col + 1, value) # BIT is 1-indexed
35 self.trees.append(bit)
36
37 def update(self, row, col, val):
38 # Updates the value at the (row, col) position to 'val'
39 diff = val - (self.trees[row].query(col + 1) - self.trees[row].query(col))
40 self.trees[row].update(col + 1, diff)
41
42 def sumRegion(self, row1, col1, row2, col2):
43 # Computes the sum of all elements in the rectangular region ((row1, col1), (row2, col2))
44 return sum(
45 tree.query(col2 + 1) - tree.query(col1)
46 for tree in self.trees[row1: row2 + 1]
47 )
48
1class BinaryIndexedTree {
2 private int size; // Represents the size of the Binary Indexed Tree
3 private int[] tree; // The array representing the tree
4
5 public BinaryIndexedTree(int size) {
6 this.size = size;
7 this.tree = new int[size + 1];
8 }
9
10 // Updates the Binary Indexed Tree with the given value 'delta' at index 'index'
11 public void update(int index, int delta) {
12 while (index <= size) {
13 tree[index] += delta;
14 index += lowBit(index);
15 }
16 }
17
18 // Queries the cumulative frequency up to index 'index'
19 public int query(int index) {
20 int sum = 0;
21 while (index > 0) {
22 sum += tree[index];
23 index -= lowBit(index);
24 }
25 return sum;
26 }
27
28 // Utility method to get the lowest significant bit of 'x'
29 private static int lowBit(int x) {
30 return x & -x;
31 }
32}
33
34class NumMatrix {
35 private BinaryIndexedTree[] trees; // Array of Binary Indexed Trees for 2D updates and queries
36
37 // Constructor that initializes the NumMatrix with a given 2D matrix
38 public NumMatrix(int[][] matrix) {
39 int rows = matrix.length;
40 int cols = matrix[0].length;
41 trees = new BinaryIndexedTree[rows];
42 for (int i = 0; i < rows; ++i) {
43 BinaryIndexedTree tree = new BinaryIndexedTree(cols);
44 for (int j = 0; j < cols; ++j) {
45 tree.update(j + 1, matrix[i][j]); // Populate the Binary Indexed Tree with initial matrix values
46 }
47 trees[i] = tree; // Associate the tree with the current row
48 }
49 }
50
51 // Updates the value at the given row and column with a new value 'val'
52 public void update(int row, int col, int val) {
53 BinaryIndexedTree tree = trees[row];
54 int currentVal = tree.query(col + 1) - tree.query(col); // Get the current value using prefix sum query
55 tree.update(col + 1, val - currentVal); // Update with the difference
56 }
57
58 // Queries the sum of elements within the rectangular region from (row1, col1) to (row2, col2)
59 public int sumRegion(int row1, int col1, int row2, int col2) {
60 int sum = 0;
61 for (int i = row1; i <= row2; ++i) {
62 BinaryIndexedTree tree = trees[i];
63 sum += tree.query(col2 + 1) - tree.query(col1); // Query each row's sum and accumulate the result
64 }
65 return sum;
66 }
67}
68
1#include <vector>
2
3using std::vector;
4
5// Binary Indexed Tree, also known as a Fenwick Tree,
6// for efficient updates and prefix sum computations.
7class BinaryIndexedTree {
8public:
9 int size;
10 vector<int> tree;
11
12 // Constructor initializes the tree with given size.
13 explicit BinaryIndexedTree(int size)
14 : size(size), tree(size + 1, 0) {}
15
16 // Updates the tree by adding 'delta' at index 'index'.
17 void Update(int index, int delta) {
18 while (index <= size) {
19 tree[index] += delta;
20 index += LowBit(index);
21 }
22 }
23
24 // Computes the prefix sum from start to index 'index'.
25 int Query(int index) {
26 int sum = 0;
27 while (index > 0) {
28 sum += tree[index];
29 index -= LowBit(index);
30 }
31 return sum;
32 }
33
34private:
35 // Helper function to get the lowest significant bit (LSB).
36 static int LowBit(int x) {
37 return x & -x;
38 }
39};
40
41// Number Matrix class for 2D region sum queries and updates.
42class NumMatrix {
43public:
44 vector<BinaryIndexedTree*> rowsTrees;
45
46 // Constructor takes a 2D matrix and initializes a Binary Indexed Tree
47 // for each row.
48 explicit NumMatrix(vector<vector<int>>& matrix) {
49 int rows = matrix.size();
50 int cols = matrix[0].size();
51 rowsTrees.resize(rows);
52 for (int i = 0; i < rows; ++i) {
53 rowsTrees[i] = new BinaryIndexedTree(cols);
54 for (int j = 0; j < cols; ++j) {
55 rowsTrees[i]->Update(j + 1, matrix[i][j]);
56 }
57 }
58 }
59
60 // Updates the value at specific row and column to be 'val'.
61 void update(int row, int col, int val) {
62 BinaryIndexedTree* tree = rowsTrees[row];
63 int prevValue = tree->Query(col + 1) - tree->Query(col);
64 tree->Update(col + 1, val - prevValue);
65 }
66
67 // Calculates the sum of the elements of matrix inside the rectangle
68 // defined by its upper left corner (row1, col1) and lower right corner (row2, col2).
69 int sumRegion(int row1, int col1, int row2, int col2) {
70 int sum = 0;
71 for (int i = row1; i <= row2; ++i) {
72 sum += rowsTrees[i]->Query(col2 + 1) - rowsTrees[i]->Query(col1);
73 }
74 return sum;
75 }
76
77 // Destructor to clean up the allocated Binary Indexed Trees.
78 ~NumMatrix() {
79 for (BinaryIndexedTree* tree: rowsTrees) {
80 delete tree;
81 }
82 }
83};
84
85// Note: Usage of NumMatrix class remains unchanged.
86
1// TypeScript does not need explicit includes.
2// Import statement for vector in TypeScript might be unnecessary because arrays are built-in.
3
4// In TypeScript, the "array" data type is used and its size is dynamic.
5
6// In a real TypeScript environment, it is better to encapsulate
7// these functionalities within a class or module.
8
9type BIT = {
10 size: number;
11 tree: number[];
12};
13
14// Initialize the Binary Indexed Tree for efficient updates and prefix sum computations.
15function createBIT(size: number): BIT {
16 return {
17 size: size,
18 tree: new Array(size + 1).fill(0)
19 };
20}
21
22// Updates the tree by adding 'delta' at index 'index'.
23function updateBIT(bit: BIT, index: number, delta: number) {
24 while (index <= bit.size) {
25 bit.tree[index] += delta;
26 index += lowBit(index);
27 }
28}
29
30// Computes the prefix sum from start to index 'index'.
31function queryBIT(bit: BIT, index: number): number {
32 let sum = 0;
33 while (index > 0) {
34 sum += bit.tree[index];
35 index -= lowBit(index);
36 }
37 return sum;
38}
39
40// Helper function to get the lowest significant bit (LSB).
41function lowBit(x: number): number {
42 return x & -x;
43}
44
45let rowsBITs: BIT[];
46
47// Create a Binary Indexed Tree for each row of the matrix.
48function initializeNumMatrix(matrix: number[][]): void {
49 const rows = matrix.length;
50 const cols = matrix[0].length;
51 rowsBITs = new Array(rows);
52
53 for (let i = 0; i < rows; ++i) {
54 rowsBITs[i] = createBIT(cols);
55 for (let j = 0; j < cols; ++j) {
56 updateBIT(rowsBITs[i], j + 1, matrix[i][j]);
57 }
58 }
59}
60
61// Updates the value at a specific row and column to be 'val'.
62function update(row: number, col: number, val: number) {
63 const tree = rowsBITs[row];
64 const prevValue = queryBIT(tree, col + 1) - queryBIT(tree, col);
65 updateBIT(tree, col + 1, val - prevValue);
66}
67
68// Calculates the sum of the elements of the matrix inside the rectangle
69// defined by its upper left corner (row1, col1) and lower right corner (row2, col2).
70function sumRegion(row1: number, col1: number, row2: number, col2: number): number {
71 let sum = 0;
72 for (let i = row1; i <= row2; ++i) {
73 sum += queryBIT(rowsBITs[i], col2 + 1) - queryBIT(rowsBITs[i], col1);
74 }
75 return sum;
76}
77
78// In TypeScript, there is no need for a destructor as memory management is handled by the runtime.
79
Time and Space Complexity
Time Complexity
Initialization (NumMatrix.__init__
):
- Let
m
be the number of rows andn
be the number of columns in the matrix. - For each row, we initialize a Binary Indexed Tree (BIT), which takes
O(n)
time. - We then update the BIT with each element, taking
O(log n)
per update. - This results in
O(m * n * log n)
time complexity for the initialization of theNumMatrix
.
Update (NumMatrix.update
):
- Computing the previous value in the BIT involves two
query
calls, each of which takesO(log n)
time. - Updating the BIT also takes
O(log n)
time. - Thus, the
update
operation has a time complexity ofO(log n)
.
Query (NumMatrix.sumRegion
):
- We perform a
query
operation for each row in the queried region. Forrow2 - row1 + 1
rows, this results in that many queries. - Each
query
involves two calls toBinaryIndexedTree.query
, each takingO(log n)
time. - The time complexity for the sumRegion operation is
O((row2 - row1 + 1) * log n)
.
Space Complexity
- Each BIT for a row requires
O(n)
space. - With
m
rows, the space complexity for storing all BITs isO(m * n)
. - The input matrix is not modified or copied, and the additional space used for the indices and temporary variables in the methods is
O(1)
. - Therefore, the space complexity of the
NumMatrix
class isO(m * n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!