Bricks Falling When Hit
HardPrompt
You are given an m x n binary grid, where each 1 represents a brick and 0 represents an empty space. A brick is stable if:
- It is directly connected to the top of the grid, or
- At least one other brick in its four adjacent cells is stable.
You are also given an array hits, which is a sequence of erasures we want to apply. Each time we want to erase the brick at the location hits[i] = (rowi, coli). The brick on that location (if it exists) will disappear. Some other bricks may no longer be stable because of that erasure and will fall. Once a brick falls, it is immediately erased from the grid (i.e., it does not land on other stable bricks).
Return an array result, where each result[i] is the number of bricks that will fall after the ith erasure is applied.
Note that an erasure may refer to a location with no brick, and if it does, no bricks drop.
Example 1:
Input: grid = [[1,0,0,0],[1,1,1,0]], hits = [[1,0]]
Output: [2]
Explanation: Starting with the grid:
[[1,0,0,0],
[1,1,1,0]]
We erase the underlined brick at (1,0), resulting in the grid:
[[1,0,0,0],
[0,1,1,0]]
The two underlined bricks are no longer stable as they are no longer connected to the top nor adjacent to another stable brick, so they will fall. The resulting grid is:
[[1,0,0,0],
[0,0,0,0]]
Hence the result is [2].Example 2:
Input: grid = [[1,0,0,0],[1,1,0,0]], hits = [[1,1],[1,0]]
Output: [0,0]
Explanation: Starting with the grid:
[[1,0,0,0],
[1,1,0,0]]
We erase the underlined brick at (1,1), resulting in the grid:
[[1,0,0,0],
[1,0,0,0]]
All remaining bricks are still stable, so no bricks fall. The grid remains the same:
[[1,0,0,0],
[1,0,0,0]]
Next, we erase the underlined brick at (1,0), resulting in the grid:
[[1,0,0,0],
[0,0,0,0]]
Once again, all remaining bricks are still stable, so no bricks fall.
Hence the result is [0,0].
Constraints:
m == grid.lengthn == grid[i].length1 <= m, n <= 200grid[i][j]is0or1.1 <= hits.length <= 4 * 104hits[i].length == 20 <= xi <= m - 10 <= yi <= n - 1- All
(xi, yi)are unique.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem. For each hit, it removes the specified brick and then re-evaluates the stability of all remaining bricks from scratch by performing a full grid traversal. This is the most straightforward but also the least efficient method.
Algorithm
- Initialize an empty
resultarray to store the number of fallen bricks for each hit. - Iterate through each
hitin thehitsarray. - Let
(r, c)be the coordinates of the current hit. - If the cell
grid[r][c]is already empty (0), it means the hit has no effect. Add 0 to theresultand continue to the next hit. - If there is a brick at
(r, c), perform the hit by settinggrid[r][c] = 0. - After the hit, we need to identify all bricks that are no longer stable and have fallen.
- Create a
stableboolean grid of the same dimensions, initialized tofalse. - Start a graph traversal (like DFS or BFS) from every brick in the top row (
grid[0][j] == 1). - During the traversal, mark all reachable bricks as
truein thestablegrid.
- Create a
- Now, count the fallen bricks.
- Initialize a
fallen_countto 0. - Iterate through the entire grid. Any cell
(i, j)that contains a brick (grid[i][j] == 1) but is not marked as stable (stable[i][j] == false) is a fallen brick. - Increment
fallen_countfor each such brick. - To ensure the grid state is correct for the next hit, permanently remove these fallen bricks by setting
grid[i][j] = 0.
- Initialize a
- Add the
fallen_countto theresultarray. - After iterating through all hits, return the
resultarray.
Walkthrough
The brute-force method iterates through each hit one by one. For each hit, it first updates the grid by removing the brick at the specified location. Then, it determines the consequences of this removal. To do this, it must find the new set of stable bricks. A brick is stable if it's connected to the top row. We can identify all stable bricks by starting a graph traversal (like DFS or BFS) from every brick in the top row and marking all reachable bricks. After identifying the stable bricks, any remaining brick in the grid is considered unstable and falls. We count these fallen bricks, add the count to our result, and permanently remove them from the grid to set up the state for the next hit. This entire process is repeated for every hit in the input array.
class Solution { public int[] hitBricks(int[][] grid, int[][] hits) { int m = grid.length; int n = grid[0].length; int[] result = new int[hits.length]; for (int i = 0; i < hits.length; i++) { int r = hits[i][0]; int c = hits[i][1]; if (grid[r][c] == 0) { result[i] = 0; continue; } grid[r][c] = 0; // Perform the hit // Find all stable bricks after the hit boolean[][] stable = new boolean[m][n]; for (int j = 0; j < n; j++) { if (grid[0][j] == 1) { dfs(0, j, grid, stable); } } // Count fallen bricks and update the grid int fallen = 0; for (int row = 0; row < m; row++) { for (int col = 0; col < n; col++) { if (grid[row][col] == 1 && !stable[row][col]) { fallen++; grid[row][col] = 0; // Remove fallen brick for next iteration } } } result[i] = fallen; } return result; } private void dfs(int r, int c, int[][] grid, boolean[][] stable) { if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] == 0 || stable[r][c]) { return; } stable[r][c] = true; int[] dr = {-1, 1, 0, 0}; int[] dc = {0, 0, -1, 1}; for (int i = 0; i < 4; i++) { dfs(r + dr[i], c + dc[i], grid, stable); } }}Complexity
Time
O(K * M * N), where K is the number of hits, and M and N are the dimensions of the grid. For each of the K hits, we may traverse the entire grid, leading to a complexity of O(M * N) per hit.
Space
O(M * N), where M and N are the dimensions of the grid. This space is used for the `stable` boolean grid and for the recursion stack in the case of DFS.
Trade-offs
Pros
Simple to understand and implement as it directly models the problem statement.
Cons
Highly inefficient due to its time complexity.
Will result in a 'Time Limit Exceeded' error for larger inputs as it recomputes the stability of the entire grid for every single hit.
This highly efficient approach avoids the costly re-computation of the brute-force method by reversing the problem. Instead of removing bricks and calculating disconnections, we start with the final state (after all hits) and add bricks back one by one in the reverse order of hits. The Union-Find data structure is perfectly suited for this, as it can efficiently track the merging of connected components and their sizes.
Algorithm
- Step 1: Mark Hits & Prepare Initial State.
- Modify the input
gridin-place. Iterate through thehitsarray. If a hit(r, c)is on a brick (grid[r][c] == 1), change its value to 2. This marks it as a brick that will be hit later. - The initial state for our reverse process is a grid containing only the bricks that are never hit (value 1).
- Modify the input
- Step 2: Initialize Union-Find.
- Create a Union-Find (DSU) data structure with
M*N + 1elements. The elements0toM*N - 1correspond to the grid cells, and the extra elementM*Nacts as a virtual 'ceiling' node, representing the source of stability. - Initialize the
sizearray such thatsize[i] = 1for grid cells andsize[ceiling] = 0.
- Create a Union-Find (DSU) data structure with
- Step 3: Build Initial Connections.
- Iterate through the grid. For every brick that was never hit (
grid[r][c] == 1), connect it to its adjacent non-hit bricks using theunionoperation. - If a non-hit brick is in the top row (
r == 0), union it with the ceiling nodeM*N. - After this step,
size[find(ceiling)]gives the number of stable bricks after all hits have occurred.
- Iterate through the grid. For every brick that was never hit (
- Step 4: Process Hits in Reverse.
- Create a
resultarray. Iterate through thehitsarray backwards (fromk-1to0). - For each hit
(r, c):- If
grid[r][c]is not 2, it means the hit was on an empty space or a brick that was already removed by a previous hit in the original sequence. The number of fallen bricks is 0. Store this and continue. - If
grid[r][c] == 2, we are 'adding' this brick back. - First, record the number of stable bricks before adding it back:
stable_before = size[find(ceiling)]. - Change
grid[r][c]from 2 to 1, effectively adding it to the grid. - Union the newly added brick with all its adjacent neighbors (that are now bricks) and with the ceiling node if it's in the top row.
- After the unions, find the new count of stable bricks:
stable_after = size[find(ceiling)]. - The number of bricks that just became stable by adding this brick is
stable_after - stable_before - 1. The-1accounts for the hit brick itself. This is the number of bricks that fell. Storemax(0, stable_after - stable_before - 1)in the result.
- If
- Create a
- Step 5: Return Result.
- The
resultarray is filled in reverse order, so it's already correct. Return it.
- The
Walkthrough
The core insight is that tracking connections (union) is much easier than tracking disconnections (splitting). We can simulate the process in reverse time.
First, we determine the final state of the grid after all hits have occurred. We do this by marking all bricks that will be hit. Then, we initialize a Union-Find data structure representing the grid cells plus a special 'ceiling' node. We build the initial set of connections by uniting all the bricks that survive all the hits. Bricks in the top row are united with the ceiling node, making them stable.
Next, we iterate through the hits array in reverse. For each hit, we 'add' the brick back to the grid. Before adding it, we query the DSU for the current number of stable bricks (the size of the component connected to the ceiling). After adding the brick back (by setting its value to 1 and uniting it with its neighbors and potentially the ceiling), we query the number of stable bricks again. The increase in the stable brick count (minus 1 for the brick we just added) represents the number of other bricks that became stable due to this addition. These are precisely the bricks that must have fallen when this brick was hit in the forward direction. We record this number and proceed to the next hit in reverse, until all hits are processed.
class Solution { private int[] parent; private int[] sz; // size of component private int m, n; private int[][] grid; private int[] dr = {0, 1, 0, -1}; private int[] dc = {1, 0, -1, 0}; public int[] hitBricks(int[][] grid, int[][] hits) { this.m = grid.length; this.n = grid[0].length; this.grid = grid; int k = hits.length; int[] ans = new int[k]; // Step 1: Mark bricks that will be hit. for (int[] hit : hits) { if (grid[hit[0]][hit[1]] == 1) { grid[hit[0]][hit[1]] = 2; // Mark as a brick to be hit } } // Step 2: Initialize DSU. int ceiling = m * n; parent = new int[m * n + 1]; sz = new int[m * n + 1]; for (int i = 0; i < m * n; i++) { parent[i] = i; sz[i] = 1; } parent[ceiling] = ceiling; sz[ceiling] = 0; // Ceiling node itself is not a brick // Step 3: Build initial connections from non-hit bricks. for (int r = 0; r < m; r++) { for (int c = 0; c < n; c++) { if (grid[r][c] == 1) { unionNeighbors(r, c); } } } // Step 4: Process hits in reverse. for (int i = k - 1; i >= 0; i--) { int r = hits[i][0]; int c = hits[i][1]; if (grid[r][c] != 2) { ans[i] = 0; } else { int stableBefore = sz[find(ceiling)]; grid[r][c] = 1; // Add the brick back unionNeighbors(r, c); int stableAfter = sz[find(ceiling)]; ans[i] = Math.max(0, stableAfter - stableBefore - 1); } } return ans; } private void unionNeighbors(int r, int c) { int idx = r * n + c; if (r == 0) { union(idx, m * n); } for (int i = 0; i < 4; i++) { int nr = r + dr[i]; int nc = c + dc[i]; if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == 1) { union(idx, nr * n + nc); } } } private int find(int i) { if (parent[i] == i) return i; return parent[i] = find(parent[i]); } private void union(int i, int j) { int rootI = find(i); int rootJ = find(j); if (rootI != rootJ) { if (sz[rootI] < sz[rootJ]) { // Union by size int temp = rootI; rootI = rootJ; rootJ = temp; } parent[rootJ] = rootI; sz[rootI] += sz[rootJ]; } }}Complexity
Time
O((M*N + K) * α(M*N)), where α is the extremely slow-growing Inverse Ackermann function. This is effectively linear time, O(M*N + K), making it very fast.
Space
O(M * N) for the Union-Find data structure's parent and size arrays. The input grid is modified in-place.
Trade-offs
Pros
Very efficient, with a near-linear time complexity.
A standard and powerful technique for dynamic connectivity problems that involve removals.
Cons
More complex to conceptualize and implement compared to the brute-force approach.
The reverse-time logic can be counter-intuitive.
Solutions
Solution
class Solution {private int[] p;private int[] size;public int[] hitBricks(int[][] grid, int[][] hits) { int m = grid.length; int n = grid[0].length; p = new int[m * n + 1]; size = new int[m * n + 1]; for (int i = 0; i < p.length; ++i) { p[i] = i; size[i] = 1; } int[][] g = new int[m][n]; for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { g[i][j] = grid[i][j]; } } for (int[] h : hits) { g[h[0]][h[1]] = 0; } for (int j = 0; j < n; ++j) { if (g[0][j] == 1) { union(j, m * n); } } for (int i = 1; i < m; ++i) { for (int j = 0; j < n; ++j) { if (g[i][j] == 0) { continue; } if (g[i - 1][j] == 1) { union(i * n + j, (i - 1) * n + j); } if (j > 0 && g[i][j - 1] == 1) { union(i * n + j, i * n + j - 1); } } } int[] ans = new int[hits.length]; int[] dirs = {-1, 0, 1, 0, -1}; for (int k = hits.length - 1; k >= 0; --k) { int i = hits[k][0]; int j = hits[k][1]; if (grid[i][j] == 0) { continue; } g[i][j] = 1; int prev = size[find(m * n)]; if (i == 0) { union(j, m * n); } for (int l = 0; l < 4; ++l) { int x = i + dirs[l]; int y = j + dirs[l + 1]; if (x >= 0 && x < m && y >= 0 && y < n && g[x][y] == 1) { union(i * n + j, x * n + y); } } int curr = size[find(m * n)]; ans[k] = Math.max(0, curr - prev - 1); } return ans; }private int find(int x) { if (p[x] != x) { p[x] = find(p[x]); } return p[x]; }private void union(int a, int b) { int pa = find(a); int pb = find(b); if (pa != pb) { size[pb] += size[pa]; p[pa] = pb; } }}class Solution {public: vector<int> p; vector<int> size; vector<int> hitBricks(vector<vector<int>> &grid, vector<vector<int>> &hits) { int m = grid.size(), n = grid[0].size(); p.resize(m * n + 1); size.resize(m * n + 1); for (int i = 0; i < p.size(); ++i) { p[i] = i; size[i] = 1; } vector<vector<int>> g(m, vector<int>(n)); for (int i = 0; i < m; ++i) for (int j = 0; j < n; ++j) g[i][j] = grid[i][j]; for (auto &h : hits) g[h[0]][h[1]] = 0; for (int j = 0; j < n; ++j) if (g[0][j] == 1) merge(j, m * n); for (int i = 1; i < m; ++i) { for (int j = 0; j < n; ++j) { if (g[i][j] == 0) continue; if (g[i - 1][j] == 1) merge(i * n + j, (i - 1) * n + j); if (j > 0 && g[i][j - 1] == 1) merge(i * n + j, i * n + j - 1); } } vector<int> ans(hits.size()); vector<int> dirs = {-1, 0, 1, 0, -1}; for (int k = hits.size() - 1; k >= 0; --k) { int i = hits[k][0], j = hits[k][1]; if (grid[i][j] == 0) continue; g[i][j] = 1; int prev = size[find(m * n)]; if (i == 0) merge(j, m * n); for (int l = 0; l < 4; ++l) { int x = i + dirs[l], y = j + dirs[l + 1]; if (x >= 0 && x < m && y >= 0 && y < n && g[x][y] == 1) merge(i * n + j, x * n + y); } int curr = size[find(m * n)]; ans[k] = max(0, curr - prev - 1); } return ans; } int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } void merge(int a, int b) { int pa = find(a), pb = find(b); if (pa != pb) { size[pb] += size[pa]; p[pa] = pb; } }};class Solution: def hitBricks(self, grid: List[List[int]], hits: List[List[int]]) -> List[int]: def find(x): if p[x] != x: p[x] = find(p[x]) return p[x] def union(a, b): pa, pb = find(a), find(b) if pa != pb: size[pb] += size[pa] p[pa] = pb m, n = len(grid), len(grid[0]) p = list(range(m * n + 1)) size = [1] * len(p) g = deepcopy(grid) for i, j in hits: g[i][j] = 0 for j in range(n): if g[0][j] == 1: union(j, m * n) for i in range(1, m): for j in range(n): if g[i][j] == 0: continue if g[i - 1][j] == 1: union(i * n + j, (i - 1) * n + j) if j > 0 and g[i][j - 1] == 1: union(i * n + j, i * n + j - 1) ans = [] for i, j in hits[:: - 1]: if grid[i][j] == 0: ans . append(0) continue g[i][j] = 1 prev = size[find(m * n)] if i == 0: union(j, m * n) 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)] ans . append(max(0, curr - prev - 1)) return ans[:: - 1]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.