Facebook Pixel

1476. Subrectangle Queries

MediumDesignArrayMatrix
Leetcode Link

Problem Description

You need to implement a class called SubrectangleQueries that manages a 2D matrix of integers and supports operations to update rectangular regions and retrieve individual values.

The class should have the following functionality:

Constructor: Takes a 2D matrix (rectangle) of integers with dimensions rows x cols as input and initializes the object with this matrix.

Method 1 - updateSubrectangle(row1, col1, row2, col2, newValue):

  • Updates all values within a specified rectangular region to newValue
  • The rectangular region is defined by:
    • Upper-left corner at coordinates (row1, col1)
    • Bottom-right corner at coordinates (row2, col2)
  • All cells within this rectangle (inclusive of boundaries) should be set to newValue

Method 2 - getValue(row, col):

  • Returns the current value at the specified position (row, col) in the matrix
  • This should reflect any updates that have been made through updateSubrectangle calls

The solution uses a clever optimization technique: instead of actually updating the matrix values during each updateSubrectangle call, it maintains a list of update operations (ops). When getValue is called, it checks the operations list in reverse order (most recent first) to see if the requested cell falls within any updated rectangle. If it does, it returns the value from the most recent applicable update. If the cell hasn't been affected by any updates, it returns the original value from the initial matrix.

This approach is efficient when there are many updates but relatively few value queries, as it avoids the overhead of actually modifying multiple cells during each update operation.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When faced with this problem, the straightforward approach would be to directly modify the matrix during each updateSubrectangle call - iterating through all cells in the specified rectangle and updating their values. However, this could be expensive if we have large rectangles to update.

The key insight is to think about what we actually need. We don't necessarily need to modify the matrix immediately - we only need to return the correct value when getValue is called. This leads us to consider a lazy evaluation strategy.

Instead of performing the actual updates, we can simply record what updates were requested. Think of it like keeping a log of changes rather than applying them immediately. When someone asks for a value at a specific position, we can then check our log to determine what the current value should be.

The crucial observation is that if multiple updates overlap at a certain cell, only the most recent update matters. For example, if cell (2, 3) is first updated to 5, then later updated to 10 as part of different rectangle updates, the value should be 10. This is why we traverse the operations list in reverse order - we want to find the most recent update that affects our target cell.

This approach trades space for time efficiency in certain scenarios:

  • If we have many updates but few queries, we save time by not actually modifying potentially thousands of cells
  • If a large rectangle is updated multiple times, we avoid redundant work
  • Each updateSubrectangle becomes an O(1) operation instead of O((row2-row1+1) * (col2-col1+1))

The trade-off is that getValue becomes O(k) where k is the number of updates, but in many practical scenarios, this is acceptable, especially when updates are frequent and queries are relatively sparse.

Solution Approach

The implementation uses a lazy evaluation pattern with an operations log to efficiently handle rectangle updates.

Data Structures:

  • self.g: Stores the original rectangle matrix passed in the constructor
  • self.ops: A list that stores tuples representing each update operation in the format (row1, col1, row2, col2, newValue)

Constructor Implementation:

def __init__(self, rectangle: List[List[int]]):
    self.g = rectangle
    self.ops = []

Simply stores the original matrix and initializes an empty operations list.

Update Operation:

def updateSubrectangle(self, row1: int, col1: int, row2: int, col2: int, newValue: int) -> None:
    self.ops.append((row1, col1, row2, col2, newValue))

Instead of modifying the matrix directly, we append the update parameters as a tuple to our operations list. This is an O(1) operation, making updates extremely fast regardless of the rectangle size.

Get Value Operation:

def getValue(self, row: int, col: int) -> int:
    for r1, c1, r2, c2, v in self.ops[::-1]:
        if r1 <= row <= r2 and c1 <= col <= c2:
            return v
    return self.g[row][col]

The retrieval logic works as follows:

  1. Iterate through the operations list in reverse order using self.ops[::-1] (most recent updates first)
  2. For each operation, check if the requested cell (row, col) falls within the update rectangle bounds:
    • Row condition: r1 <= row <= r2
    • Column condition: c1 <= col <= c2
  3. If both conditions are met, the cell was affected by this update, so return the corresponding value v
  4. If we traverse all operations without finding a match, the cell was never updated, so return the original value from self.g[row][col]

Time Complexity:

  • updateSubrectangle: O(1) - just appending to a list
  • getValue: O(k) where k is the number of update operations performed

Space Complexity:

  • O(m*n + k) where m*n is the size of the original matrix and k is the number of update operations stored

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate how this solution works.

Initial Setup:

rectangle = [[1, 2, 1],
             [4, 3, 4],
             [3, 2, 1]]

We create a SubrectangleQueries object with this 3×3 matrix:

  • self.g stores the original matrix
  • self.ops is initialized as an empty list []

Operation 1: updateSubrectangle(0, 0, 2, 1, 5)

This updates the rectangle from (0,0) to (2,1) with value 5. Instead of modifying the matrix directly, we append the operation to our list:

  • self.ops = [(0, 0, 2, 1, 5)]

The conceptual matrix would look like:

[[5, 5, 1],
 [5, 5, 4],
 [5, 5, 1]]

But we haven't actually modified self.g.

Operation 2: getValue(0, 2)

We want the value at position (0, 2):

  1. Check operations in reverse order
  2. Operation (0, 0, 2, 1, 5): Is 0 ≤ 0 ≤ 2? Yes. Is 0 ≤ 2 ≤ 1? No.
  3. Cell (0, 2) is not in this rectangle, so check original matrix
  4. Return self.g[0][2] = 1

Operation 3: updateSubrectangle(1, 1, 2, 2, 10)

Append another operation:

  • self.ops = [(0, 0, 2, 1, 5), (1, 1, 2, 2, 10)]

The conceptual matrix would now be:

[[5, 5, 1],
 [5, 10, 10],
 [5, 10, 10]]

Operation 4: getValue(1, 1)

We want the value at position (1, 1):

  1. Check operations in reverse order
  2. Operation (1, 1, 2, 2, 10): Is 1 ≤ 1 ≤ 2? Yes. Is 1 ≤ 1 ≤ 2? Yes.
  3. Cell (1, 1) is in this rectangle, return 10 immediately

Notice we found the value without checking the first operation - this is the efficiency gain when cells are affected by multiple updates.

Operation 5: getValue(2, 0)

We want the value at position (2, 0):

  1. Check operation (1, 1, 2, 2, 10): Is 1 ≤ 2 ≤ 2? Yes. Is 1 ≤ 0 ≤ 2? No.
  2. Check operation (0, 0, 2, 1, 5): Is 0 ≤ 2 ≤ 2? Yes. Is 0 ≤ 0 ≤ 1? Yes.
  3. Cell (2, 0) is in this rectangle, return 5

This demonstrates how the algorithm correctly handles overlapping updates by checking the most recent operations first.

Solution Implementation

1from typing import List
2
3
4class SubrectangleQueries:
5    """
6    A class to handle subrectangle queries on a 2D matrix.
7    Uses lazy evaluation by storing update operations instead of modifying the matrix directly.
8    """
9  
10    def __init__(self, rectangle: List[List[int]]) -> None:
11        """
12        Initialize the data structure with the given rectangle matrix.
13      
14        Args:
15            rectangle: A 2D list representing the initial matrix values
16        """
17        self.matrix = rectangle
18        # Store update operations as a list of tuples (row1, col1, row2, col2, new_value)
19        self.update_history = []
20
21    def updateSubrectangle(
22        self, row1: int, col1: int, row2: int, col2: int, newValue: int
23    ) -> None:
24        """
25        Update all values in the subrectangle defined by (row1, col1) to (row2, col2) with newValue.
26        Uses lazy evaluation by recording the operation instead of updating the matrix directly.
27      
28        Args:
29            row1: Top row index of the subrectangle
30            col1: Left column index of the subrectangle
31            row2: Bottom row index of the subrectangle
32            col2: Right column index of the subrectangle
33            newValue: The new value to set for all cells in the subrectangle
34        """
35        # Append the update operation to history
36        self.update_history.append((row1, col1, row2, col2, newValue))
37
38    def getValue(self, row: int, col: int) -> int:
39        """
40        Get the current value at position (row, col).
41        Checks update history in reverse order to find the most recent update affecting this cell.
42      
43        Args:
44            row: Row index of the cell
45            col: Column index of the cell
46          
47        Returns:
48            The current value at the specified position
49        """
50        # Iterate through update history in reverse order (most recent first)
51        for top_row, left_col, bottom_row, right_col, value in reversed(self.update_history):
52            # Check if the current cell falls within this update's subrectangle
53            if top_row <= row <= bottom_row and left_col <= col <= right_col:
54                return value
55      
56        # If no updates affected this cell, return the original value
57        return self.matrix[row][col]
58
59
60# Your SubrectangleQueries object will be instantiated and called as such:
61# obj = SubrectangleQueries(rectangle)
62# obj.updateSubrectangle(row1,col1,row2,col2,newValue)
63# param_2 = obj.getValue(row,col)
64
1class SubrectangleQueries {
2    // Original rectangle matrix
3    private int[][] rectangle;
4    // List to store update operations in reverse chronological order (most recent first)
5    private LinkedList<int[]> updateOperations;
6
7    /**
8     * Constructor to initialize the SubrectangleQueries with the given rectangle
9     * @param rectangle The initial 2D matrix of integers
10     */
11    public SubrectangleQueries(int[][] rectangle) {
12        this.rectangle = rectangle;
13        this.updateOperations = new LinkedList<>();
14    }
15
16    /**
17     * Updates all values in the subrectangle defined by (row1, col1) to (row2, col2) with newValue
18     * Instead of actually updating the matrix, we store the operation for lazy evaluation
19     * @param row1 Top row index of the subrectangle
20     * @param col1 Left column index of the subrectangle
21     * @param row2 Bottom row index of the subrectangle
22     * @param col2 Right column index of the subrectangle
23     * @param newValue The new value to set for all cells in the subrectangle
24     */
25    public void updateSubrectangle(int row1, int col1, int row2, int col2, int newValue) {
26        // Add the update operation to the front of the list (most recent updates first)
27        updateOperations.addFirst(new int[] {row1, col1, row2, col2, newValue});
28    }
29
30    /**
31     * Gets the current value at position (row, col)
32     * Checks update operations from most recent to oldest, returns the first matching update
33     * If no update covers this position, returns the original value
34     * @param row The row index to query
35     * @param col The column index to query
36     * @return The current value at the specified position
37     */
38    public int getValue(int row, int col) {
39        // Iterate through update operations from most recent to oldest
40        for (int[] operation : updateOperations) {
41            int topRow = operation[0];
42            int leftCol = operation[1];
43            int bottomRow = operation[2];
44            int rightCol = operation[3];
45            int value = operation[4];
46          
47            // Check if the queried position falls within this update's subrectangle
48            if (topRow <= row && row <= bottomRow && leftCol <= col && col <= rightCol) {
49                return value;
50            }
51        }
52      
53        // No update operation covers this position, return original value
54        return rectangle[row][col];
55    }
56}
57
58/**
59 * Your SubrectangleQueries object will be instantiated and called as such:
60 * SubrectangleQueries obj = new SubrectangleQueries(rectangle);
61 * obj.updateSubrectangle(row1,col1,row2,col2,newValue);
62 * int param_2 = obj.getValue(row,col);
63 */
64
1class SubrectangleQueries {
2private:
3    // Original rectangle matrix
4    vector<vector<int>> rectangle;
5  
6    // Store all update operations in chronological order
7    // Each operation: {row1, col1, row2, col2, newValue}
8    vector<vector<int>> updateOperations;
9
10public:
11    /**
12     * Constructor - Initialize with given rectangle matrix
13     * @param rectangle: Initial 2D matrix of integers
14     */
15    SubrectangleQueries(vector<vector<int>>& rectangle) {
16        this->rectangle = rectangle;
17    }
18  
19    /**
20     * Update all values in the subrectangle with newValue
21     * Instead of updating the matrix directly, we store the operation
22     * @param row1: Top row index of subrectangle
23     * @param col1: Left column index of subrectangle
24     * @param row2: Bottom row index of subrectangle
25     * @param col2: Right column index of subrectangle
26     * @param newValue: New value to set for all cells in subrectangle
27     */
28    void updateSubrectangle(int row1, int col1, int row2, int col2, int newValue) {
29        // Store the update operation instead of modifying the matrix
30        updateOperations.push_back({row1, col1, row2, col2, newValue});
31    }
32  
33    /**
34     * Get the current value at position (row, col)
35     * Check operations from most recent to oldest
36     * @param row: Row index of the target cell
37     * @param col: Column index of the target cell
38     * @return: Current value at the specified position
39     */
40    int getValue(int row, int col) {
41        // Iterate through operations in reverse order (most recent first)
42        for (int i = updateOperations.size() - 1; i >= 0; --i) {
43            vector<int>& operation = updateOperations[i];
44            int topRow = operation[0];
45            int leftCol = operation[1];
46            int bottomRow = operation[2];
47            int rightCol = operation[3];
48            int value = operation[4];
49          
50            // Check if current cell is within this operation's subrectangle
51            if (topRow <= row && row <= bottomRow && 
52                leftCol <= col && col <= rightCol) {
53                // Return the value from the most recent applicable update
54                return value;
55            }
56        }
57      
58        // No update operation affects this cell, return original value
59        return rectangle[row][col];
60    }
61};
62
63/**
64 * Your SubrectangleQueries object will be instantiated and called as such:
65 * SubrectangleQueries* obj = new SubrectangleQueries(rectangle);
66 * obj->updateSubrectangle(row1,col1,row2,col2,newValue);
67 * int param_2 = obj->getValue(row,col);
68 */
69
1// Store the original grid
2let grid: number[][];
3// Store all update operations in chronological order
4let operations: number[][];
5
6/**
7 * Initialize the data structure with the given rectangle
8 * @param rectangle - 2D array representing the initial grid values
9 */
10function SubrectangleQueries(rectangle: number[][]): void {
11    grid = rectangle;
12    operations = [];
13}
14
15/**
16 * Update all values in the subrectangle defined by (row1, col1) to (row2, col2) with newValue
17 * Instead of updating the grid directly, we store the operation for lazy evaluation
18 * @param row1 - Top row index of the subrectangle
19 * @param col1 - Left column index of the subrectangle
20 * @param row2 - Bottom row index of the subrectangle
21 * @param col2 - Right column index of the subrectangle
22 * @param newValue - The new value to set for all cells in the subrectangle
23 */
24function updateSubrectangle(
25    row1: number,
26    col1: number,
27    row2: number,
28    col2: number,
29    newValue: number
30): void {
31    // Store the update operation
32    operations.push([row1, col1, row2, col2, newValue]);
33}
34
35/**
36 * Get the current value at position (row, col)
37 * Check operations in reverse order (most recent first) to find the latest update
38 * @param row - Row index of the target cell
39 * @param col - Column index of the target cell
40 * @returns The current value at the specified position
41 */
42function getValue(row: number, col: number): number {
43    // Iterate through operations from most recent to oldest
44    for (let i = operations.length - 1; i >= 0; i--) {
45        const [topRow, leftCol, bottomRow, rightCol, value] = operations[i];
46      
47        // Check if the current cell falls within this update's subrectangle
48        if (topRow <= row && row <= bottomRow && leftCol <= col && col <= rightCol) {
49            // Return the value from the most recent update that affects this cell
50            return value;
51        }
52    }
53  
54    // If no updates affected this cell, return the original value
55    return grid[row][col];
56}
57

Time and Space Complexity

Time Complexity:

  • __init__(self, rectangle): O(1) - Simply stores the reference to the rectangle and initializes an empty list.
  • updateSubrectangle(row1, col1, row2, col2, newValue): O(1) - Appends a single tuple to the operations list.
  • getValue(row, col): O(k) where k is the number of update operations performed so far. In the worst case, we iterate through all stored operations in reverse order.

Space Complexity:

  • Overall space: O(m*n + k) where:
    • m*n is the space for storing the initial rectangle (m rows, n columns)
    • k is the number of update operations stored in the ops list
  • Each update operation stores 5 values (row1, col1, row2, col2, newValue), contributing O(5k) = O(k) additional space.

Key Design Choice: This implementation uses a "lazy update" strategy where updates are not immediately applied to the rectangle matrix. Instead, each update is recorded as an operation. When retrieving a value, the code checks the most recent updates first (by iterating in reverse order) to find if the requested position was affected by any update. This makes updates very fast O(1) at the cost of slower reads O(k).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Memory Overhead with Excessive Updates

The lazy evaluation approach stores every update operation in memory. If there are millions of update operations but very few getValue calls, this can lead to significant memory consumption and slower getValue performance over time.

Solution: Implement a threshold-based optimization where after a certain number of operations (e.g., 1000), the updates are applied to the matrix and the operations list is cleared:

def updateSubrectangle(self, row1: int, col1: int, row2: int, col2: int, newValue: int) -> None:
    self.update_history.append((row1, col1, row2, col2, newValue))
  
    # Apply updates if history grows too large
    if len(self.update_history) > 1000:
        self._apply_updates()

def _apply_updates(self):
    for r1, c1, r2, c2, val in self.update_history:
        for i in range(r1, r2 + 1):
            for j in range(c1, c2 + 1):
                self.matrix[i][j] = val
    self.update_history.clear()

2. Incorrect Boundary Checking

A common mistake is using exclusive boundaries instead of inclusive ones when checking if a cell falls within an update rectangle. The problem specifies inclusive boundaries for both corners.

Incorrect:

if r1 <= row < r2 and c1 <= col < c2:  # Wrong! Uses exclusive upper bounds

Correct:

if r1 <= row <= r2 and c1 <= col <= c2:  # Correct: inclusive boundaries

3. Not Iterating Updates in Reverse Order

The most recent update should take precedence. Forgetting to reverse the operations list will return values from the oldest applicable update instead of the newest.

Incorrect:

for r1, c1, r2, c2, v in self.update_history:  # Wrong order!
    if r1 <= row <= r2 and c1 <= col <= c2:
        return v

Correct:

for r1, c1, r2, c2, v in reversed(self.update_history):  # Most recent first
    if r1 <= row <= r2 and c1 <= col <= c2:
        return v

4. Performance Degradation Pattern

While the lazy evaluation is efficient for write-heavy workloads, it becomes inefficient when getValue is called frequently on different cells. Each getValue call has O(k) complexity where k is the number of updates.

Solution: Consider implementing a hybrid approach based on access patterns:

class SubrectangleQueries:
    def __init__(self, rectangle: List[List[int]]):
        self.matrix = rectangle
        self.update_history = []
        self.get_count = 0
        self.update_count = 0
  
    def getValue(self, row: int, col: int) -> int:
        self.get_count += 1
      
        # If reads are becoming more frequent than writes, apply updates
        if self.get_count > self.update_count * 10 and len(self.update_history) > 0:
            self._apply_updates()
          
        # Continue with normal getValue logic...

5. Not Handling Edge Cases

Failing to validate that the rectangle coordinates are valid or that row1 ≤ row2 and col1 ≤ col2.

Solution: Add validation in the update method:

def updateSubrectangle(self, row1: int, col1: int, row2: int, col2: int, newValue: int) -> None:
    # Ensure valid rectangle bounds
    row1, row2 = min(row1, row2), max(row1, row2)
    col1, col2 = min(col1, col2), max(col1, col2)
  
    # Optional: validate against matrix dimensions
    if row2 >= len(self.matrix) or col2 >= len(self.matrix[0]):
        raise ValueError("Rectangle extends beyond matrix bounds")
  
    self.update_history.append((row1, col1, row2, col2, newValue))
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More