并查集 逆向操作的题目 细思极恐(leetcode 803题 打砖块)

首先描述下题目:
有一个 m x n 的二元网格,其中 1 表示砖块,0 表示空白。砖块 稳定(不会掉落)的前提是:

一块砖直接连接到网格的顶部,或者
至少有一块相邻(4 个方向之一)砖块 稳定 不会掉落时
给你一个数组 hits ,这是需要依次消除砖块的位置。每当消除 hits[i] = (rowi, coli) 位置上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而掉落。一旦砖块掉落,它会立即从网格中消失(即,它不会落在其他稳定的砖块上)。

返回一个数组 result ,其中 result[i] 表示第 i 次消除操作对应掉落的砖块数目。

注意,消除可能指向是没有砖块的空白位置,如果发生这种情况,则没有砖块掉落。

sample 1:

输入:grid = [[1,0,0,0],[1,1,1,0]], hits = [[1,0]]
输出:[2]
解释:
网格开始为:
[[1,0,0,0][1,1,1,0]]
消除 (1,0) 处加粗的砖块,得到网格:
[[1,0,0,0]
 [0,1,1,0]]
两个加粗的砖不再稳定,因为它们不再与顶部相连,也不再与另一个稳定的砖相邻,因此它们将掉落。得到网格:
[[1,0,0,0],
 [0,0,0,0]]
因此,结果为 [2]

还没看懂,没关系我们有
sample 2

输入:grid = [[1,0,0,0],[1,1,0,0]], hits = [[1,1],[1,0]]
输出:[0,0]
解释:
网格开始为:
[[1,0,0,0],
 [1,1,0,0]]
消除 (1,1) 处加粗的砖块,得到网格:
[[1,0,0,0],
 [1,0,0,0]]
剩下的砖都很稳定,所以不会掉落。网格保持不变:
[[1,0,0,0], 
 [1,0,0,0]]
接下来消除 (1,0) 处加粗的砖块,得到网格:
[[1,0,0,0],
 [0,0,0,0]]
剩下的砖块仍然是稳定的,所以不会有砖块掉落。
因此,结果为 [0,0]

这下能理解个大概了吧。

本题出题人思路我猜的哈

思路就是首先你要理解并查集是啥,然后把并查集逆向输出,一顿操作后就可以出本道题,再让你反向操作回去,像极了我们学倒车入库,教练把车从库里面倒出来,我们再倒进去,十分困难。

废话不多说 上代码:

class Solution {

    private int rows;
    private int cols;

    // 方向数组,用于访问网格中某个坐标点上下左右位置的操作(上右下左)
    public static final int[][] DIRECTIONS  = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};

    public int[] hitBricks(int[][] grid, int[][] hits) {
        /*
        * 消除一个砖块的效果是:一个连通分量被分成了两个连通分量;
        * 并查集的作用是:把两个连通分量合并成一个连通分量。
        * 故想用并查集 需要逆向操作这个问题。
        */
        this.rows = grid.length;
        this.cols = grid[0].length;

        // step 3 把grid网格中的砖头全部击碎,这里我们copy一份后敲碎副本
        int[][] copy = new int[rows][cols];
        for(int i = 0; i < rows; ++i) {
            for(int j = 0; j < cols; j++) {
                copy[i][j] = grid[i][j];
            }
        }

        // 把copy敲碎(所有需要敲碎的1变成0)
        for(int[] hit : hits) {
            copy[hit[0]][hit[1]] = 0;
        }

        // step 4 把砖块之间的关系输入并查集,size表示二维空间网格大小,也表示稳定的屋顶
        int size = rows * cols;
        UnionFind803 unionFind803 = new UnionFind803(size + 1); // 因为要表示屋顶所以要+1

        // 将横坐标为0的行砖块与屋顶相连
        for(int j =0; j < cols; ++j) {
            if(copy[0][j] == 1) {
                unionFind803.union(j, size);
            }
        }

        // 其余网格位置砖块只考虑向上向左 是否有砖块相连(题目规律),故在并查集中进行合并
        for(int i = 1; i < rows; ++i) {
            for(int j = 0; j < cols; ++j) {
                if(copy[i][j] == 1) {
                    // 先判断上面是否有砖块相连
                    if(copy[i - 1][j] == 1) {
                        unionFind803.union(getIndex(i - 1, j), getIndex(i , j));
                    }
                    // 再判断左边是否有砖块相连
                    if(j > 0 && copy[i][j - 1] == 1) {
                        unionFind803.union(getIndex(i, j - 1), getIndex(i , j));
                    }
                }
            }
        }

        // step 5 按照hits的逆序, 在copy中补回砖块, 把每一次因为补回砖块而能够与屋顶相连的
        // 砖块的增量记录到res数组中
        int hitsLength = hits.length;
        int[] res = new int[hitsLength];
        for(int i = hitsLength - 1; i >= 0; --i) {
            int x = hits[i][0];
            int y = hits[i][1];

            // 如果原grid中这一块位置本来就没有砖块,那就敲了个寂寞,也不会有砖块会因此掉落,故跳过
            if(grid[x][y] == 0) {
                continue;
            }

            // 反向补回与屋顶相连的砖块数量
            int origin = unionFind803.getSize(size);

            // 如果补的砖块是第一行则与屋顶直接相连
            if(x == 0) {
                unionFind803.union(y, size);
            }

            // 上右下左看下有没有相邻砖块可以合并
            for(int[] direction : DIRECTIONS) {
                int newX = x + direction[0];
                int newY = y + direction[1];

                //  没越界且有砖块可以连接
                if(inArea(newX, newY) && copy[newX][newY] == 1) {
                    unionFind803.union(getIndex(x, y), getIndex(newX, newY));
                }
            }

            // 补回之后与屋顶相连的砖块数
            int current = unionFind803.getSize(size);
            // 最后用敲击前后与屋顶相连砖块个数减去1, 与0比较大小(可能存在添加当前砖块不会使其与屋顶
            // 相连的砖块数更多,减去1会得出-1这个值,而最小应该是0,没有任何增加)
            res[i] = Math.max(0, current - origin - 1);

            // 然后补回这个砖块是其恢复最先开始的原样
            copy[x][y] = 1;
        }
        return res;    
    }

    // 判断输入的二维坐标是否越界
    private boolean inArea(int x, int y) {
        return x >= 0 && x < rows && y >= 0 && y < cols;
    }

    // 由于并查集操作时一维坐标的操作,本题图是二维坐标,所以需要做降为操作
    private int getIndex(int x, int y) {
        return x * cols + y;
    }

    // step 1 由于本题设计连通性问题,考虑用并查集实现
    private class UnionFind803 {
        // 当前节点的父节点
        private int[] parent;
        // 以当前节点为根节点的子树的个数
        private int[] size;

        public UnionFind803(int n) {
            parent = new int[n];
            size = new int[n];
            for(int i = 0; i < n; ++i) {
                parent[i] = i;
                size[i] = 1;
            }
        }

        // 路径压缩
        public int find(int x) {
            if(x != parent[x]) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);

            if(rootX == rootY) {
                return;
            }
            parent[rootX] = rootY;
            // 合并时维护数组size
            size[rootY] += size[rootX];
        }

        // step 2 返回x在并查集根节点的子树包含的节点的总数
        public int getSize(int x) {
            int root = find(x);
            return size[root];
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值