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.
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:
- Start with the final state (all hits applied)
- Create a virtual "super root" representing the ceiling
- Connect all top-row bricks to this super root
- Build the initial connected components
- 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 sizem * n + 1
where each cell(i, j)
maps to indexi * 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 EvaluatorExample 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))
- First row union with virtual root:
- 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))
- Finding the size before adding the brick:
- 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
usesO(m * n + 1)
space - The size array uses
O(m * n + 1)
space - The copied grid
g
usesO(m * n)
space - The answer array uses
O(k)
space - Total space complexity is
O(m * n)
since typicallyk ≤ 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.
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
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Coding Interview 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
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 assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!