Facebook Pixel

803. Bricks Falling When Hit

Problem Description

You have a grid of bricks represented as an m x n binary matrix where 1 represents a brick and 0 represents empty space. A brick remains stable (doesn't fall) if it meets one of these conditions:

  • It's in the top row (directly connected to the ceiling), OR
  • At least one of its 4 adjacent neighbors (up, down, left, right) is stable

You're given a list of hits, where each hits[i] = (row_i, col_i) represents erasing the brick at that position. When you erase a brick:

  • If there's no brick at that position, nothing happens
  • If there's a brick, it disappears
  • Any bricks that become unstable as a result will immediately fall and disappear (they don't land on other bricks below)

Your task is to return an array where result[i] represents how many bricks fall after applying the i-th hit.

Key Points:

  • Bricks fall if they lose their connection to the top row (either directly or through other stable bricks)
  • When bricks fall, they completely disappear from the grid
  • A hit on an empty cell causes no bricks to fall
  • The hits are applied sequentially, and each hit affects the grid state left by previous hits

Example scenario: If hitting a brick causes a cluster of bricks to lose their only connection to the top row, all bricks in that cluster will fall and be counted in the result for that hit.

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

Intuition

The key insight is to think about this problem in reverse. Instead of removing bricks and watching them fall, we can start with all the hits already applied (all bricks already removed), then add the bricks back one by one in reverse order.

Why does this help? When we remove a brick, it's hard to track which bricks will fall because we need to check if they still have a path to the top. But when we add a brick back, we can easily count how many previously disconnected bricks now become connected to the top through this newly added brick.

Think of it like this: imagine a group of floating bricks that aren't connected to the ceiling. When we place a brick that bridges them to the ceiling, all those floating bricks suddenly become stable. The number of bricks that "would have fallen" when we removed this brick is exactly the number of floating bricks that became stable when we added it back.

This naturally leads us to use Union-Find (Disjoint Set Union) data structure. We can treat all bricks connected to the top row as one giant component. Here's the process:

  1. Start with the final state (all hits applied)
  2. Create a virtual "super root" representing the ceiling
  3. Connect all top-row bricks to this super root
  4. Build the initial connected components
  5. Process hits in reverse order:
    • When adding a brick back, check how many bricks were in the "ceiling component" before
    • Add the brick and connect it to its neighbors
    • Check how many bricks are in the "ceiling component" after
    • The difference (minus 1 for the brick we just added) tells us how many bricks would have fallen when this brick was originally removed

The beauty of this approach is that Union-Find efficiently tracks component sizes, so we can quickly determine how many previously disconnected bricks become connected to the ceiling when we add each brick back.

Learn more about Union Find patterns.

Solution Approach

The solution implements the reverse-time Union-Find approach. Let's walk through the implementation step by step:

1. Union-Find Setup:

  • Create a parent array p of size m * n + 1 where each cell (i, j) maps to index i * n + j
  • The extra position m * n serves as the "super root" representing the ceiling
  • Initialize size array to track component sizes

2. Apply All Hits First:

g = deepcopy(grid)
for i, j in hits:
    g[i][j] = 0

Create a copy of the grid and remove all bricks that will be hit, giving us the final state.

3. Build Initial Connected Components:

# Connect top row bricks to the super root
for j in range(n):
    if g[0][j] == 1:
        union(j, m * n)

# Connect remaining bricks to their neighbors
for i in range(1, m):
    for j in range(n):
        if g[i][j] == 0:
            continue
        if g[i - 1][j] == 1:  # Connect to brick above
            union(i * n + j, (i - 1) * n + j)
        if j > 0 and g[i][j - 1] == 1:  # Connect to brick on left
            union(i * n + j, i * n + j - 1)

We only check up and left directions to avoid duplicate unions (the down and right neighbors will check up and left respectively).

4. Process Hits in Reverse:

for i, j in hits[::-1]:
    if grid[i][j] == 0:  # Original position had no brick
        ans.append(0)
        continue
  
    g[i][j] = 1  # Add brick back
    prev = size[find(m * n)]  # Size of ceiling component before
  
    if i == 0:
        union(j, m * n)  # Connect to ceiling if in top row
  
    # Connect to all 4 neighbors
    for a, b in [(-1, 0), (1, 0), (0, 1), (0, -1)]:
        x, y = i + a, j + b
        if 0 <= x < m and 0 <= y < n and g[x][y] == 1:
            union(i * n + j, x * n + y)
  
    curr = size[find(m * n)]  # Size of ceiling component after
    ans.append(max(0, curr - prev - 1))

For each hit in reverse:

  • If the original grid had no brick at this position, 0 bricks fall
  • Otherwise, add the brick back and measure the change in the ceiling component size
  • The number of bricks that fell = (new size - old size - 1), where we subtract 1 for the brick we just added
  • Use max(0, ...) to handle edge cases where the brick wasn't connected to ceiling

5. Return Results in Original Order:

return ans[::-1]

Since we processed hits in reverse, reverse the answer array to match the original hit order.

Time Complexity: O(k * α(m*n)) where k is the number of hits and α is the inverse Ackermann function (practically constant)

Space Complexity: O(m * n) for the Union-Find structure and grid copy

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's work through a small example to illustrate the solution approach.

Initial Setup:

Grid (2x3):        Hits: [(1,1), (0,1)]
[1, 1, 1]          
[0, 1, 0]          

Step 1: Apply all hits to get final state

After all hits:
[1, 0, 1]
[0, 0, 0]

Step 2: Build Union-Find with ceiling connection

  • Create super root at index 6 (since we have 2×3 = 6 cells)
  • Connect top row bricks to ceiling:
    • Cell (0,0) → index 0 → connects to 6
    • Cell (0,2) → index 2 → connects to 6
  • No other bricks to connect to neighbors

Current state: Two bricks connected to ceiling through super root 6. Component size at root 6: 3 (brick at index 0 + brick at index 2 + super root)

Step 3: Process hits in reverse order

Processing Hit #2 (originally at index 1): Position (0,1)

  • Original grid had brick here (grid[0][1] = 1)
  • Previous ceiling size: 3
  • Add brick back at (0,1):
    [1, 1, 1]
    [0, 0, 0]
  • Since it's in top row, connect to ceiling (index 1 → 6)
  • Check 4 neighbors:
    • Left (0,0): has brick, union(1, 0) - already connected via ceiling
    • Right (0,2): has brick, union(1, 2) - already connected via ceiling
  • New ceiling size: 4
  • Bricks that fell: 4 - 3 - 1 = 0

Processing Hit #1 (originally at index 0): Position (1,1)

  • Original grid had brick here (grid[1][1] = 1)
  • Previous ceiling size: 4
  • Add brick back at (1,1):
    [1, 1, 1]
    [0, 1, 0]
  • Not in top row, don't connect directly to ceiling
  • Check 4 neighbors:
    • Up (0,1): has brick, union(4, 1) - connects this brick to ceiling!
  • New ceiling size: 5
  • Bricks that fell: 5 - 4 - 1 = 0

Step 4: Reverse the results Results in reverse: [0, 0] Final answer: [0, 0]

Why this makes sense:

  • Hit at (1,1): The brick at (1,1) was connected to ceiling through (0,1), so removing it doesn't cause any falls
  • Hit at (0,1): This is a ceiling brick surrounded by other ceiling bricks, removing it doesn't disconnect anything

Let's consider a different example where bricks actually fall:

Example 2:

Grid (3x2):        Hits: [(1,0), (0,0)]
[1, 1]          
[1, 0]
[1, 0]          

After applying all hits:

[0, 1]
[0, 0]
[1, 0]

The brick at (2,0) is now disconnected from ceiling!

Processing in reverse:

  • Add back (0,0): Connects (2,0) to ceiling through (1,0) → 1 brick saved from falling
  • Add back (1,0): Just connects to existing ceiling component → 0 bricks fall

Final answer: [0, 1] (reversed from [1, 0])

This shows how the reverse approach elegantly counts falling bricks by measuring how many disconnected bricks become connected when we add each brick back.

Solution Implementation

1class Solution:
2    def hitBricks(self, grid: List[List[int]], hits: List[List[int]]) -> List[int]:
3        """
4        Calculate how many bricks fall after each hit using Union-Find.
5        Strategy: Process hits in reverse order to simulate adding bricks back.
6        """
7      
8        def find(node: int) -> int:
9            """Find root of node with path compression."""
10            if parent[node] != node:
11                parent[node] = find(parent[node])
12            return parent[node]
13      
14        def union(node_a: int, node_b: int) -> None:
15            """Union two nodes by connecting their roots."""
16            root_a, root_b = find(node_a), find(node_b)
17            if root_a != root_b:
18                # Merge smaller component into larger one
19                component_size[root_b] += component_size[root_a]
20                parent[root_a] = root_b
21      
22        rows, cols = len(grid), len(grid[0])
23      
24        # Initialize Union-Find structure
25        # Index rows * cols represents the virtual "ceiling" node
26        ceiling_node = rows * cols
27        parent = list(range(rows * cols + 1))
28        component_size = [1] * (rows * cols + 1)
29      
30        # Create grid state after all hits are applied
31        grid_after_hits = [row[:] for row in grid]  # Deep copy
32        for row_idx, col_idx in hits:
33            grid_after_hits[row_idx][col_idx] = 0
34      
35        # Connect all bricks in top row to ceiling
36        for col_idx in range(cols):
37            if grid_after_hits[0][col_idx] == 1:
38                union(col_idx, ceiling_node)
39      
40        # Connect all remaining bricks to their neighbors
41        for row_idx in range(1, rows):
42            for col_idx in range(cols):
43                if grid_after_hits[row_idx][col_idx] == 0:
44                    continue
45                  
46                current_node = row_idx * cols + col_idx
47              
48                # Connect to brick above if exists
49                if grid_after_hits[row_idx - 1][col_idx] == 1:
50                    above_node = (row_idx - 1) * cols + col_idx
51                    union(current_node, above_node)
52              
53                # Connect to brick on left if exists
54                if col_idx > 0 and grid_after_hits[row_idx][col_idx - 1] == 1:
55                    left_node = row_idx * cols + col_idx - 1
56                    union(current_node, left_node)
57      
58        result = []
59      
60        # Process hits in reverse order (adding bricks back)
61        for row_idx, col_idx in reversed(hits):
62            # If original grid had no brick here, no bricks can fall
63            if grid[row_idx][col_idx] == 0:
64                result.append(0)
65                continue
66          
67            # Add brick back
68            grid_after_hits[row_idx][col_idx] = 1
69            current_node = row_idx * cols + col_idx
70          
71            # Count stable bricks before adding this brick
72            stable_bricks_before = component_size[find(ceiling_node)]
73          
74            # If brick is in top row, connect to ceiling
75            if row_idx == 0:
76                union(col_idx, ceiling_node)
77          
78            # Connect to all adjacent bricks
79            directions = [(-1, 0), (1, 0), (0, 1), (0, -1)]  # up, down, right, left
80            for delta_row, delta_col in directions:
81                adjacent_row = row_idx + delta_row
82                adjacent_col = col_idx + delta_col
83              
84                # Check if adjacent position is valid and has a brick
85                if (0 <= adjacent_row < rows and 
86                    0 <= adjacent_col < cols and 
87                    grid_after_hits[adjacent_row][adjacent_col] == 1):
88                    adjacent_node = adjacent_row * cols + adjacent_col
89                    union(current_node, adjacent_node)
90          
91            # Count stable bricks after adding this brick
92            stable_bricks_after = component_size[find(ceiling_node)]
93          
94            # Calculate fallen bricks (exclude the added brick itself)
95            fallen_bricks = max(0, stable_bricks_after - stable_bricks_before - 1)
96            result.append(fallen_bricks)
97      
98        # Reverse result to match original hit order
99        return result[::-1]
100
1class Solution {
2    // Parent array for Union-Find data structure
3    private int[] parent;
4    // Size array to track component sizes
5    private int[] componentSize;
6
7    public int[] hitBricks(int[][] grid, int[][] hits) {
8        int rows = grid.length;
9        int cols = grid[0].length;
10      
11        // Initialize Union-Find with extra node for "roof" (index: rows * cols)
12        parent = new int[rows * cols + 1];
13        componentSize = new int[rows * cols + 1];
14        for (int i = 0; i < parent.length; ++i) {
15            parent[i] = i;
16            componentSize[i] = 1;
17        }
18      
19        // Create a copy of the grid to work with
20        int[][] workingGrid = new int[rows][cols];
21        for (int i = 0; i < rows; ++i) {
22            for (int j = 0; j < cols; ++j) {
23                workingGrid[i][j] = grid[i][j];
24            }
25        }
26      
27        // Remove all bricks that will be hit
28        for (int[] hit : hits) {
29            workingGrid[hit[0]][hit[1]] = 0;
30        }
31      
32        // Connect all top row bricks to the virtual roof node
33        for (int col = 0; col < cols; ++col) {
34            if (workingGrid[0][col] == 1) {
35                union(col, rows * cols);
36            }
37        }
38      
39        // Build initial Union-Find structure for remaining bricks
40        for (int row = 1; row < rows; ++row) {
41            for (int col = 0; col < cols; ++col) {
42                if (workingGrid[row][col] == 0) {
43                    continue;
44                }
45                // Connect with brick above if exists
46                if (workingGrid[row - 1][col] == 1) {
47                    union(row * cols + col, (row - 1) * cols + col);
48                }
49                // Connect with brick to the left if exists
50                if (col > 0 && workingGrid[row][col - 1] == 1) {
51                    union(row * cols + col, row * cols + col - 1);
52                }
53            }
54        }
55      
56        // Process hits in reverse order to simulate adding bricks back
57        int[] result = new int[hits.length];
58        int[] directions = {-1, 0, 1, 0, -1}; // For traversing 4 directions
59      
60        for (int k = hits.length - 1; k >= 0; --k) {
61            int hitRow = hits[k][0];
62            int hitCol = hits[k][1];
63          
64            // Skip if original position had no brick
65            if (grid[hitRow][hitCol] == 0) {
66                continue;
67            }
68          
69            // Add brick back
70            workingGrid[hitRow][hitCol] = 1;
71          
72            // Record size of roof component before adding this brick
73            int previousSize = componentSize[find(rows * cols)];
74          
75            // If brick is in top row, connect it to roof
76            if (hitRow == 0) {
77                union(hitCol, rows * cols);
78            }
79          
80            // Connect with adjacent bricks in all 4 directions
81            for (int dir = 0; dir < 4; ++dir) {
82                int newRow = hitRow + directions[dir];
83                int newCol = hitCol + directions[dir + 1];
84              
85                if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols 
86                    && workingGrid[newRow][newCol] == 1) {
87                    union(hitRow * cols + hitCol, newRow * cols + newCol);
88                }
89            }
90          
91            // Calculate newly connected bricks (current size - previous size - the added brick itself)
92            int currentSize = componentSize[find(rows * cols)];
93            result[k] = Math.max(0, currentSize - previousSize - 1);
94        }
95      
96        return result;
97    }
98
99    /**
100     * Find operation with path compression for Union-Find
101     * @param x the node to find root for
102     * @return root of the component containing x
103     */
104    private int find(int x) {
105        if (parent[x] != x) {
106            parent[x] = find(parent[x]); // Path compression
107        }
108        return parent[x];
109    }
110
111    /**
112     * Union operation for Union-Find
113     * Merges two components and updates size
114     * @param a first node
115     * @param b second node
116     */
117    private void union(int a, int b) {
118        int rootA = find(a);
119        int rootB = find(b);
120      
121        if (rootA != rootB) {
122            // Merge component A into component B
123            componentSize[rootB] += componentSize[rootA];
124            parent[rootA] = rootB;
125        }
126    }
127}
128
1class Solution {
2public:
3    vector<int> parent;      // Union-Find parent array
4    vector<int> groupSize;   // Size of each connected component
5  
6    vector<int> hitBricks(vector<vector<int>>& grid, vector<vector<int>>& hits) {
7        int rows = grid.size();
8        int cols = grid[0].size();
9      
10        // Initialize Union-Find structure
11        // Index rows * cols represents the "roof" (virtual node connected to all top row bricks)
12        parent.resize(rows * cols + 1);
13        groupSize.resize(rows * cols + 1);
14        for (int i = 0; i < parent.size(); ++i) {
15            parent[i] = i;
16            groupSize[i] = 1;
17        }
18      
19        // Create a copy of the grid with all hits applied
20        vector<vector<int>> currentGrid(rows, vector<int>(cols));
21        for (int i = 0; i < rows; ++i) {
22            for (int j = 0; j < cols; ++j) {
23                currentGrid[i][j] = grid[i][j];
24            }
25        }
26      
27        // Remove all bricks that will be hit
28        for (auto& hit : hits) {
29            currentGrid[hit[0]][hit[1]] = 0;
30        }
31      
32        // Connect all top row bricks to the roof (virtual node at index rows * cols)
33        for (int j = 0; j < cols; ++j) {
34            if (currentGrid[0][j] == 1) {
35                merge(j, rows * cols);
36            }
37        }
38      
39        // Build initial connected components for remaining bricks
40        for (int i = 1; i < rows; ++i) {
41            for (int j = 0; j < cols; ++j) {
42                if (currentGrid[i][j] == 0) continue;
43              
44                // Connect to brick above if it exists
45                if (currentGrid[i - 1][j] == 1) {
46                    merge(i * cols + j, (i - 1) * cols + j);
47                }
48              
49                // Connect to brick on the left if it exists
50                if (j > 0 && currentGrid[i][j - 1] == 1) {
51                    merge(i * cols + j, i * cols + j - 1);
52                }
53            }
54        }
55      
56        // Process hits in reverse order to simulate adding bricks back
57        vector<int> result(hits.size());
58        vector<int> directions = {-1, 0, 1, 0, -1};  // Used for 4-directional movement
59      
60        for (int k = hits.size() - 1; k >= 0; --k) {
61            int row = hits[k][0];
62            int col = hits[k][1];
63          
64            // If original grid had no brick at this position, no bricks fall
65            if (grid[row][col] == 0) continue;
66          
67            // Add the brick back
68            currentGrid[row][col] = 1;
69          
70            // Get the size of stable bricks before adding this brick
71            int previousStableSize = groupSize[find(rows * cols)];
72          
73            // If brick is in top row, connect it to the roof
74            if (row == 0) {
75                merge(col, rows * cols);
76            }
77          
78            // Connect to all adjacent bricks
79            for (int dir = 0; dir < 4; ++dir) {
80                int newRow = row + directions[dir];
81                int newCol = col + directions[dir + 1];
82              
83                if (newRow >= 0 && newRow < rows && 
84                    newCol >= 0 && newCol < cols && 
85                    currentGrid[newRow][newCol] == 1) {
86                    merge(row * cols + col, newRow * cols + newCol);
87                }
88            }
89          
90            // Calculate how many bricks became stable after adding this brick
91            int currentStableSize = groupSize[find(rows * cols)];
92            // Subtract 1 to exclude the brick we just added
93            result[k] = max(0, currentStableSize - previousStableSize - 1);
94        }
95      
96        return result;
97    }
98  
99    // Find operation with path compression
100    int find(int x) {
101        if (parent[x] != x) {
102            parent[x] = find(parent[x]);  // Path compression
103        }
104        return parent[x];
105    }
106  
107    // Union operation - merge two sets
108    void merge(int a, int b) {
109        int rootA = find(a);
110        int rootB = find(b);
111      
112        if (rootA != rootB) {
113            // Always merge smaller tree to larger tree (union by size)
114            groupSize[rootB] += groupSize[rootA];
115            parent[rootA] = rootB;
116        }
117    }
118};
119
1// Global variables for Union-Find structure
2let parent: number[] = [];      // Union-Find parent array
3let groupSize: number[] = [];   // Size of each connected component
4
5function hitBricks(grid: number[][], hits: number[][]): number[] {
6    const rows = grid.length;
7    const cols = grid[0].length;
8  
9    // Initialize Union-Find structure
10    // Index rows * cols represents the "roof" (virtual node connected to all top row bricks)
11    const totalSize = rows * cols + 1;
12    parent = new Array(totalSize);
13    groupSize = new Array(totalSize);
14    for (let i = 0; i < totalSize; i++) {
15        parent[i] = i;
16        groupSize[i] = 1;
17    }
18  
19    // Create a copy of the grid with all hits applied
20    const currentGrid: number[][] = [];
21    for (let i = 0; i < rows; i++) {
22        currentGrid[i] = [];
23        for (let j = 0; j < cols; j++) {
24            currentGrid[i][j] = grid[i][j];
25        }
26    }
27  
28    // Remove all bricks that will be hit
29    for (const hit of hits) {
30        currentGrid[hit[0]][hit[1]] = 0;
31    }
32  
33    // Connect all top row bricks to the roof (virtual node at index rows * cols)
34    for (let j = 0; j < cols; j++) {
35        if (currentGrid[0][j] === 1) {
36            merge(j, rows * cols);
37        }
38    }
39  
40    // Build initial connected components for remaining bricks
41    for (let i = 1; i < rows; i++) {
42        for (let j = 0; j < cols; j++) {
43            if (currentGrid[i][j] === 0) continue;
44          
45            // Connect to brick above if it exists
46            if (currentGrid[i - 1][j] === 1) {
47                merge(i * cols + j, (i - 1) * cols + j);
48            }
49          
50            // Connect to brick on the left if it exists
51            if (j > 0 && currentGrid[i][j - 1] === 1) {
52                merge(i * cols + j, i * cols + j - 1);
53            }
54        }
55    }
56  
57    // Process hits in reverse order to simulate adding bricks back
58    const result: number[] = new Array(hits.length).fill(0);
59    const directions = [-1, 0, 1, 0, -1];  // Used for 4-directional movement
60  
61    for (let k = hits.length - 1; k >= 0; k--) {
62        const row = hits[k][0];
63        const col = hits[k][1];
64      
65        // If original grid had no brick at this position, no bricks fall
66        if (grid[row][col] === 0) continue;
67      
68        // Add the brick back
69        currentGrid[row][col] = 1;
70      
71        // Get the size of stable bricks before adding this brick
72        const previousStableSize = groupSize[find(rows * cols)];
73      
74        // If brick is in top row, connect it to the roof
75        if (row === 0) {
76            merge(col, rows * cols);
77        }
78      
79        // Connect to all adjacent bricks
80        for (let dir = 0; dir < 4; dir++) {
81            const newRow = row + directions[dir];
82            const newCol = col + directions[dir + 1];
83          
84            if (newRow >= 0 && newRow < rows && 
85                newCol >= 0 && newCol < cols && 
86                currentGrid[newRow][newCol] === 1) {
87                merge(row * cols + col, newRow * cols + newCol);
88            }
89        }
90      
91        // Calculate how many bricks became stable after adding this brick
92        const currentStableSize = groupSize[find(rows * cols)];
93        // Subtract 1 to exclude the brick we just added
94        result[k] = Math.max(0, currentStableSize - previousStableSize - 1);
95    }
96  
97    return result;
98}
99
100// Find operation with path compression
101function find(x: number): number {
102    if (parent[x] !== x) {
103        parent[x] = find(parent[x]);  // Path compression
104    }
105    return parent[x];
106}
107
108// Union operation - merge two sets
109function merge(a: number, b: number): void {
110    const rootA = find(a);
111    const rootB = find(b);
112  
113    if (rootA !== rootB) {
114        // Always merge smaller tree to larger tree (union by size)
115        groupSize[rootB] += groupSize[rootA];
116        parent[rootA] = rootB;
117    }
118}
119

Time and Space Complexity

Time Complexity: O(k * m * n * α(m * n)) where k is the number of hits, m and n are the dimensions of the grid, and α is the inverse Ackermann function.

  • Initial setup and copying the grid takes O(m * n) time
  • Removing all hit bricks: O(k) time
  • Building the initial union-find structure after removing hits:
    • First row union with virtual root: O(n * α(m * n))
    • Remaining cells union operations: O(m * n * α(m * n))
  • Processing hits in reverse order: For each of the k hits:
    • Finding the size before adding the brick: O(α(m * n))
    • Union operations with up to 4 neighbors: O(4 * α(m * n))
    • Finding the size after adding the brick: O(α(m * n))
    • Total per hit: O(α(m * n))
  • Overall for all hits: O(k * α(m * n))

Since α(m * n) is practically constant (at most 4 for any reasonable input size), the time complexity can be considered O(m * n + k) in practice, but theoretically it's O((m * n + k) * α(m * n)).

Space Complexity: O(m * n)

  • The parent array p uses O(m * n + 1) space
  • The size array uses O(m * n + 1) space
  • The copied grid g uses O(m * n) space
  • The answer array uses O(k) space
  • Total space complexity is O(m * n) since typically k ≤ m * n

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

Common Pitfalls

1. Not Checking if Original Grid Had a Brick

Pitfall: A common mistake is forgetting to check whether the original grid actually had a brick at the hit position. If you process a hit on an already empty cell (where grid[i][j] == 0 originally), you might incorrectly calculate fallen bricks when reversing the operation.

Why it happens: When processing hits in reverse, we add bricks back. But if there was never a brick at that position originally, we shouldn't add one back.

Incorrect approach:

for row_idx, col_idx in reversed(hits):
    # WRONG: Not checking if original grid had a brick
    grid_after_hits[row_idx][col_idx] = 1
    # ... rest of the logic

Correct approach:

for row_idx, col_idx in reversed(hits):
    if grid[row_idx][col_idx] == 0:  # Original grid had no brick
        result.append(0)
        continue
  
    grid_after_hits[row_idx][col_idx] = 1  # Only add if originally present
    # ... rest of the logic

2. Incorrect Calculation of Fallen Bricks

Pitfall: Forgetting to subtract 1 from the difference in ceiling component sizes, which leads to counting the newly added brick itself as a "fallen" brick.

Why it happens: When we add a brick back and it connects to the ceiling, the ceiling component size increases. This increase includes both the newly added brick AND any previously disconnected bricks that now reconnect through it.

Incorrect approach:

fallen_bricks = stable_bricks_after - stable_bricks_before
# WRONG: This counts the added brick as fallen

Correct approach:

fallen_bricks = max(0, stable_bricks_after - stable_bricks_before - 1)
# Subtract 1 to exclude the brick we just added
# Use max(0, ...) to handle cases where brick doesn't connect to ceiling

3. Union-Find Path Compression Bug

Pitfall: Implementing path compression incorrectly by not updating the parent pointer during the recursive find operation.

Incorrect approach:

def find(node: int) -> int:
    if parent[node] != node:
        return find(parent[node])  # WRONG: No path compression
    return parent[node]

Correct approach:

def find(node: int) -> int:
    if parent[node] != node:
        parent[node] = find(parent[node])  # Compress path
    return parent[node]

4. Modifying Original Grid Instead of Using a Copy

Pitfall: Directly modifying the input grid when applying all hits initially, which corrupts the data needed to check if a position originally had a brick.

Incorrect approach:

# WRONG: Modifying original grid
for i, j in hits:
    grid[i][j] = 0

# Later when processing in reverse:
if grid[i][j] == 0:  # This check is now meaningless!
    result.append(0)

Correct approach:

# Create a copy to preserve original grid
grid_after_hits = [row[:] for row in grid]
for i, j in hits:
    grid_after_hits[i][j] = 0

# Original grid remains intact for checking
if grid[i][j] == 0:  # Can correctly check original state
    result.append(0)

5. Double-Counting Connections When Building Initial Components

Pitfall: Connecting bricks in all four directions when building the initial Union-Find structure, which unnecessarily doubles the work and could lead to subtle bugs if not handled consistently.

Why it happens: If you check all 4 neighbors for each brick, you'll process each edge twice (once from each endpoint).

Less efficient approach:

# Checking all 4 directions for every brick
for i in range(m):
    for j in range(n):
        for di, dj in [(0,1), (0,-1), (1,0), (-1,0)]:
            # This processes each edge twice

More efficient approach:

# Only check up and left (or down and right)
for i in range(1, m):
    for j in range(n):
        if grid_after_hits[i][j] == 0:
            continue
        # Check only up
        if grid_after_hits[i-1][j] == 1:
            union(i*n+j, (i-1)*n+j)
        # Check only left
        if j > 0 and grid_after_hits[i][j-1] == 1:
            union(i*n+j, i*n+j-1)

These pitfalls are particularly tricky because they might not cause obvious errors in simple test cases but will fail on edge cases or larger inputs.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

You are given an array of intervals where intervals[i] = [start_i, end_i] represent the start and end of the ith interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.


Recommended Readings

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

Load More