A reverse simulation and union-find solution for counting how many bricks fall after each hit.
Problem Restatement
We are given an m x n grid.
Each cell is either:
| Value | Meaning |
|---|---|
1 | Brick |
0 | Empty space |
A brick is stable if it is connected to the top row, either directly or through other 4-directionally adjacent stable bricks.
We are also given a list of hits. Each hit removes the brick at position (row, col) if a brick exists there.
After each hit, some bricks may no longer be stable. Those bricks fall and disappear. The brick directly hit is not counted as a falling brick.
Return an array where answer[i] is the number of bricks that fall after hits[i] is applied. The erasures are applied sequentially. If a hit targets an empty cell, no bricks fall.
Input and Output
| Item | Meaning |
|---|---|
| Input | A binary grid grid and a list of hit positions hits |
| Output | Number of falling bricks after each hit |
| Stable brick | A brick connected to the top row |
| Falling brick | A brick that loses connection to the top row |
| Important detail | The erased brick itself is not counted as falling |
Examples
Example 1:
grid = [[1, 0, 0, 0],
[1, 1, 1, 0]]
hits = [[1, 0]]If we remove the brick at (1, 0), the bricks at (1, 1) and (1, 2) lose their connection to the top row.
So the answer is:
[2]Example 2:
grid = [[1, 0, 0, 0],
[1, 1, 0, 0]]
hits = [[1, 1], [1, 0]]First, remove (1, 1). No other brick falls.
Then remove (1, 0). The brick (1, 1) is already gone, so nothing else falls.
The answer is:
[0, 0]First Thought: Brute Force
A direct simulation is possible.
For each hit:
- Remove the hit brick if it exists.
- Start from all bricks in the top row.
- Use DFS or BFS to mark every stable brick.
- Every remaining unmarked brick falls.
- Count those falling bricks and remove them.
This matches the problem definition, but it is too slow.
If the grid has m * n cells and there are k hits, this can cost:
O(k * m * n)Each hit may force us to scan the whole grid again.
Key Insight
Removing bricks is hard because Union-Find does not support deleting edges easily.
Adding bricks is much easier.
So we process the problem backward.
Instead of applying hits from first to last, we do this:
- Remove all hit bricks first.
- Build the connected components of the remaining grid.
- Add hit bricks back in reverse order.
- Count how many bricks newly become connected to the top.
This works because a brick that becomes stable when added back in reverse corresponds to a brick that would have fallen when that hit happened forward.
We also add a virtual roof node.
Every brick in the top row is connected to this roof node. Then the size of the roof component tells us how many bricks are stable.
Algorithm
First, copy the grid:
working = [row[:] for row in grid]Then apply all hits to the copy:
for r, c in hits:
working[r][c] = 0This gives the final grid after all hits.
Create a Union-Find over all grid cells, plus one extra node called roof.
Map a cell (r, c) to an integer id:
cell_id = r * n + cLet:
roof = m * nBuild components from the final grid:
- For every brick in the top row, union it with
roof. - For every brick, union it with adjacent bricks that also exist.
Then process hits in reverse.
For each hit (r, c):
- If
grid[r][c] == 0, the original cell was empty, so append0. - Otherwise, record the size of the roof component before adding the brick.
- Add the brick back to
working. - If it is in the top row, union it with
roof. - Union it with every existing neighboring brick.
- Record the size of the roof component after adding the brick.
- The number of fallen bricks is:
max(0, after - before - 1)We subtract 1 because the brick we added back is the brick that was directly hit. The problem does not count that brick as falling.
Finally, reverse the answers.
Correctness
After all hits are applied, working represents the grid state after the full forward process, except that fallen bricks are not explicitly simulated. This is enough for the reverse algorithm because only bricks still present after removals are included in the initial Union-Find structure.
The Union-Find structure groups adjacent bricks into connected components. The virtual roof node is connected to every top-row brick, so a brick is stable exactly when it belongs to the same component as roof.
When we process a hit backward, we restore one brick that existed in the original grid. If the original cell was empty, that hit removed nothing in the forward direction, so the correct result is 0.
Otherwise, adding the brick back may connect some previously unstable components to the roof component. The number of bricks newly connected to the roof is the increase in the roof component size.
This increase includes the restored brick itself. In the forward direction, that restored brick is the brick directly erased by the hit, so it should not be counted as falling. Therefore the number of falling bricks is after - before - 1, clipped at zero.
Every forward hit corresponds to exactly one reverse restoration, and the reverse process handles the hits in the opposite order. Thus every result computed in reverse matches the number of bricks that would fall after the corresponding forward hit.
Complexity
Let:
N = m * nand let k be the number of hits.
| Metric | Value | Why |
|---|---|---|
| Time | O((N + k) α(N)) | Union-Find operations are nearly constant time |
| Space | O(N) | Parent array, size array, and copied grid |
Here, α(N) is the inverse Ackermann function, which grows so slowly that it is effectively constant for practical input sizes.
Implementation
class DSU:
def __init__(self, size: int):
self.parent = list(range(size))
self.component_size = [1] * size
def find(self, x: int) -> int:
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]
return x
def union(self, a: int, b: int) -> None:
root_a = self.find(a)
root_b = self.find(b)
if root_a == root_b:
return
if self.component_size[root_a] < self.component_size[root_b]:
root_a, root_b = root_b, root_a
self.parent[root_b] = root_a
self.component_size[root_a] += self.component_size[root_b]
def size(self, x: int) -> int:
return self.component_size[self.find(x)]
class Solution:
def hitBricks(self, grid: list[list[int]], hits: list[list[int]]) -> list[int]:
m = len(grid)
n = len(grid[0])
roof = m * n
def index(r: int, c: int) -> int:
return r * n + c
working = [row[:] for row in grid]
for r, c in hits:
working[r][c] = 0
dsu = DSU(m * n + 1)
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
for r in range(m):
for c in range(n):
if working[r][c] == 0:
continue
if r == 0:
dsu.union(index(r, c), roof)
for dr, dc in directions:
nr = r + dr
nc = c + dc
if 0 <= nr < m and 0 <= nc < n and working[nr][nc] == 1:
dsu.union(index(r, c), index(nr, nc))
answer_reversed = []
for r, c in reversed(hits):
if grid[r][c] == 0:
answer_reversed.append(0)
continue
before = dsu.size(roof)
working[r][c] = 1
if r == 0:
dsu.union(index(r, c), roof)
for dr, dc in directions:
nr = r + dr
nc = c + dc
if 0 <= nr < m and 0 <= nc < n and working[nr][nc] == 1:
dsu.union(index(r, c), index(nr, nc))
after = dsu.size(roof)
fallen = max(0, after - before - 1)
answer_reversed.append(fallen)
return answer_reversed[::-1]Code Explanation
The DSU class stores connected components.
self.parent = list(range(size))
self.component_size = [1] * sizeEach node starts as its own component.
The find function returns the root of a component and applies path compression:
while self.parent[x] != x:
self.parent[x] = self.parent[self.parent[x]]
x = self.parent[x]The union function merges two components:
self.parent[root_b] = root_a
self.component_size[root_a] += self.component_size[root_b]The larger component remains the root. This keeps the tree shallow.
In the main solution, we create a virtual roof node:
roof = m * nAll real grid cells use ids from 0 to m * n - 1, so m * n is safe for the extra node.
We copy the grid:
working = [row[:] for row in grid]Then remove all hit bricks from the copy:
for r, c in hits:
working[r][c] = 0This lets us build the Union-Find structure from the final state.
When building components, every top-row brick connects to the roof:
if r == 0:
dsu.union(index(r, c), roof)Every adjacent pair of bricks is also connected.
During reverse processing, if the original grid had no brick at the hit location, the answer is zero:
if grid[r][c] == 0:
answer_reversed.append(0)
continueOtherwise, we measure the roof component size before restoration:
before = dsu.size(roof)Then we add the brick back and union it with the roof or neighboring bricks.
After that, we measure again:
after = dsu.size(roof)The difference tells us how many bricks newly became stable. We subtract one for the restored brick itself:
fallen = max(0, after - before - 1)Finally, because hits were processed backward, we reverse the result:
return answer_reversed[::-1]Testing
def run_tests():
s = Solution()
assert s.hitBricks(
[[1, 0, 0, 0],
[1, 1, 1, 0]],
[[1, 0]]
) == [2]
assert s.hitBricks(
[[1, 0, 0, 0],
[1, 1, 0, 0]],
[[1, 1], [1, 0]]
) == [0, 0]
assert s.hitBricks(
[[1]],
[[0, 0]]
) == [0]
assert s.hitBricks(
[[0]],
[[0, 0]]
) == [0]
assert s.hitBricks(
[[1, 1, 1],
[0, 1, 0],
[0, 1, 0]],
[[1, 1]]
) == [1]
print("all tests passed")
run_tests()| Test | Why |
|---|---|
| Sample with falling chain | Counts bricks disconnected from the roof |
| Sequential hits | Checks that later hits see earlier removals |
| Single brick | Hit brick itself is not counted |
| Empty cell hit | No brick removed, no fall |
| Vertical support removal | Checks reverse restoration and roof-size difference |