LeetCode 803. 打砖块
题目描述
✅ 803. 打砖块



题意分析
砖块只有直接位于顶行,或能沿四方向相邻砖块连到顶行,才保持稳定。依次敲击给定位置,统计每次因此失去支撑而掉落的其他砖块数,不包含直接敲掉的一块;敲到空位时答案为 0。题目保证敲击位置互不相同。
解法:逆向并查集 + 虚拟顶部
核心思路
[!blue]
正向删去一块砖可能让连通块分裂,普通并查集不能直接维护这种拆分。把过程倒过来,从移除所有敲击位置的副本开始逐块恢复,就只会增加节点和连接,可以用并查集合并。
额外建立编号为
m * n的虚拟顶部节点,把所有顶行砖块与它相连。它所在集合中的真实砖块恰好是稳定砖块。集合根可能因合并改变,所以查询规模时要先find(top),不能直接读取固定下标的规模。先复制原网格,只把所有敲击位置置为 0,再合并剩余相邻砖块。这个副本保留了悬空部分,并不等同于正向执行后的实际棋盘;只有与虚拟顶部连通的部分代表稳定状态。不能提前删掉悬空砖块,否则恢复支撑时就无法把它们一起接回顶部。
倒序处理第
k次敲击时,副本相当于原网格只移除了第0..k个敲击位置。恢复当前位置并合并四邻后,就撤销了这次删除。设顶部集合规模从prev变为cur,增加的稳定砖块就是正向这一步失去顶部连接的砖块,其中包含直接恢复的这一块,所以额外掉落数为cur - prev - 1。如果恢复后仍不连通顶部,规模增量为 0,应返回 0,所以统一取
max(0, cur - prev - 1)。原网格本就没有砖的敲击直接跳过;若原来有砖、却在更早的正向敲击中已经掉落,此时恢复它仍无法连回顶部,也自然贡献 0。虚拟顶部节点始终存在于集合中,在前后差值中会抵消。初次建图只需检查上方、左方,每条无向相邻边都会被处理一次;倒序恢复时邻居可能出现在任意方向,必须检查全部四邻。合并相同根时不重复累加规模,避免多条连接重复计数。
解题步骤
- 复制网格,并将所有命中的位置从副本移除。
- 合并副本中的相邻砖块,将顶行连接到虚拟顶部。
- 倒序处理命中位置:原网格该位置没有砖块则答案为零。
- 恢复砖块并合并四邻,记录顶部大小增量减一后的非负值。
代码实现
class Solution {
private int[] parent;
private int[] size;
public int[] hitBricks(int[][] grid, int[][] hits) {
int m = grid.length;
int n = grid[0].length;
// 额外节点代表屋顶,所有顶行砖块与它相连。
int top = m * n;
int[][] copy = new int[m][n];
for (int i = 0; i < m; i++) {
copy[i] = grid[i].clone();
}
for (int[] h : hits) {
copy[h[0]][h[1]] = 0;
}
parent = new int[top + 1];
size = new int[top + 1];
for (int i = 0; i <= top; i++) {
parent[i] = i;
size[i] = 1;
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (copy[i][j] != 1) {
continue;
}
int id = idx(i, j, n);
if (i == 0) {
union(id, top);
}
// 初建图只看上方和左方即可覆盖全部相邻边;恢复时再检查四邻。
if (i > 0 && copy[i - 1][j] == 1) {
union(id, idx(i - 1, j, n));
}
if (j > 0 && copy[i][j - 1] == 1) {
union(id, idx(i, j - 1, n));
}
}
}
int[] answer = new int[hits.length];
int[][] dirs = {
{0, 1},
{0, -1},
{1, 0},
{-1, 0},
};
for (int k = hits.length - 1; k >= 0; k--) {
int r = hits[k][0];
int c = hits[k][1];
if (grid[r][c] == 0) {
continue;
}
// 恢复前记录顶部集合大小,只统计本次新增的稳定部分。
int prev = getSize(top);
copy[r][c] = 1;
int id = idx(r, c, n);
if (r == 0) {
union(id, top);
}
for (int[] d : dirs) {
int nr = r + d[0];
int nc = c + d[1];
if (nr < 0 || nr >= m || nc < 0 || nc >= n) {
continue;
}
if (copy[nr][nc] == 1) {
union(id, idx(nr, nc, n));
}
}
int cur = getSize(top);
// 顶部增量扣除刚恢复的砖块,悬空恢复的答案取零。
answer[k] = Math.max(0, cur - prev - 1);
}
return answer;
}
private int idx(int r, int c, int n) {
return r * n + c;
}
private int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
private void union(int x, int y) {
int px = find(x);
int py = find(y);
if (px == py) {
return;
}
if (size[px] < size[py]) {
int tmp = px;
px = py;
py = tmp;
}
parent[py] = px;
size[px] += size[py];
}
private int getSize(int x) {
return size[find(x)];
}
}
type UnionFind struct {
parent []int
size []int
}
func newUnionFind(n int) *UnionFind {
parent := make([]int, n)
size := make([]int, n)
for i := range parent {
parent[i] = i
size[i] = 1
}
return &UnionFind{parent, size}
}
func (uf *UnionFind) find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.find(uf.parent[x])
}
return uf.parent[x]
}
func (uf *UnionFind) union(x, y int) {
px, py := uf.find(x), uf.find(y)
if px == py {
return
}
if uf.size[px] < uf.size[py] {
px, py = py, px
}
uf.parent[py] = px
uf.size[px] += uf.size[py]
}
func (uf *UnionFind) getSize(x int) int {
return uf.size[uf.find(x)]
}
func hitBricks(grid [][]int, hits [][]int) []int {
m, n := len(grid), len(grid[0])
// 额外节点代表屋顶,所有顶行砖块与它相连。
top := m * n
idx := func(r, c int) int { return r*n + c }
copy2 := make([][]int, m)
for i := range copy2 {
copy2[i] = make([]int, n)
copy(copy2[i], grid[i])
}
for _, hit := range hits {
copy2[hit[0]][hit[1]] = 0
}
uf := newUnionFind(m*n + 1)
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if copy2[i][j] == 1 {
if i == 0 {
uf.union(top, idx(i, j))
}
// 初建图只看上方和左方即可覆盖全部相邻边;恢复时再检查四邻。
if i > 0 && copy2[i-1][j] == 1 {
uf.union(idx(i, j), idx(i-1, j))
}
if j > 0 && copy2[i][j-1] == 1 {
uf.union(idx(i, j), idx(i, j-1))
}
}
}
}
dirs := [][2]int{
{0, 1},
{0, -1},
{1, 0},
{-1, 0},
}
result := make([]int, len(hits))
for k := len(hits) - 1; k >= 0; k-- {
r, c := hits[k][0], hits[k][1]
if grid[r][c] == 0 {
continue
}
// 恢复前记录顶部集合大小,只统计本次新增的稳定部分。
prevTop := uf.getSize(top)
copy2[r][c] = 1
if r == 0 {
uf.union(idx(r, c), top)
}
for _, d := range dirs {
nr, nc := r+d[0], c+d[1]
if nr >= 0 && nr < m && nc >= 0 && nc < n && copy2[nr][nc] == 1 {
uf.union(idx(r, c), idx(nr, nc))
}
}
currTop := uf.getSize(top)
// 顶部增量扣除刚恢复的砖块,悬空恢复的答案取零。
if currTop-prevTop-1 > 0 {
result[k] = currTop - prevTop - 1
}
}
return result
}
复杂度分析
- 时间复杂度:网格有 mn 个位置、q 次操作时,时间为 $O((mn+q)\alpha(mn+1))$。
- 空间复杂度:$O(mn)$,网格副本及并查集,另有 $O(q)$ 输出。
关键点总结
[!green]
- 顶部集合大小的变化决定新增稳定砖块数。
- 被恢复的砖块不计入掉落答案,因此需要减一并与零取最大值。
- 原网格有砖不代表正向轮到该次敲击时它仍未掉落。
易错点总结
[!yellow]
- 正向使用普通并查集直接删除节点:无法正确拆分原有连通分量。
- 使用当前砖块所在集合大小代替顶部增量:悬空集合并不稳定。
- 忘记排除直接敲掉的砖块:答案多算一。
- 将副本中的悬空砖块提前全部清除:破坏倒序恢复所需的连接关系。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 305. 岛屿数量 II | 困难 | 动态加点比动态删点更适合并查集,本题可倒序恢复砖块,把删除转为合并。 |
| 827. 最大人工岛 | 困难 | 同样先维护连通块大小,本题关注与屋顶连通的变化,原题关注一次加点后的最大岛屿。 |