【LeetCode】﹝并查集ி﹞连通分量个数(套用模板一直爽)

【LeetCode】﹝并查集ி﹞连通分量个数(套用模板一直爽)

模板(使用路径压缩的加权quick-union算法)

详细并查集的知识见往期博客 高级数据结构(Ⅰ)并查集(Union-Find)

    class UF{
        int N;
        int count;
        int[] id;
        int[] sz;

        UF(int N){
            this.N = N;
            count = N;
            id = new int[N];
            sz = new int[N];

            for(int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount() {
            return count;
        }

        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if(pRoot != qRoot) {
                if(sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = id[qRoot];
                    sz[qRoot] += sz[pRoot];
                }else {
                    id[qRoot] = id[pRoot];
                    sz[pRoot] += sz[qRoot];
                }
                count--;
            }
        }

        private int find(int p) {
            if(p == id[p]) return p;
            id[p] = find(id[p]);
            return id[p];
        }
    }

连通网络的操作次数★★

1319. 连通网络的操作次数

题目】用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 ab

网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。

给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1

提示

  • 1 <= n <= 10^5
  • 1 <= connections.length <= min(n*(n-1)/2, 10^5)
  • connections[i].length == 2
  • 0 <= connections[i][0], connections[i][1] < n
  • connections[i][0] != connections[i][1]
  • 没有重复的连接。
  • 两台计算机不会通过多条线缆连接。

示例

示例1:
输入:n = 4, connections = [[0,1],[0,2],[1,2]]
输出:1
解释:拔下计算机 12 之间的线缆,并将它插到计算机 13 上。

在这里插入图片描述

示例2:
输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2]]
输出:-1
解释:线缆数量不足。

解题思路

利用并查集求出多余的缆线数量和连通分量的个数(java耗时6ms)

class Solution {
    public int makeConnected(int n, int[][] connections) {
        UF uf = new UF(n);
        int ress = 0;
        for (int[] connection : connections) {
            int t = uf.union(connection[0], connection[1]);
            if (t == -1) {
                ress++;
            }
        }
        return ress >= uf.getCount() - 1 ? uf.getCount() - 1 : -1;
    }

    class UF {
        int N;
        int count;
        int[] id;
        int[] sz;

        private UF (int n) {
            N = n;
            count = n;
            id = new int[N];
            sz = new int[N];
            for (int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount () {
            return count;
        }

        public int union (int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot != qRoot) {
                if (sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = qRoot;
                    sz[qRoot] += sz[pRoot];
                } else {
                    id[qRoot] = pRoot;
                    sz[pRoot] += sz[qRoot];
                }
                count--;
                return 1;
            } else {
                return -1;
            }
        }

        private int find (int p) {
            if (p == id[p]) {
                return p;
            }
            id[p] = find(id[p]);
            return id[p];
        }
    }
}

模板因题测试而异,如测试数据量较大,适合用上述模板,若测试数据量较小,使用如下简单版本的并查集

(java耗时3ms)

class Solution {
    public int makeConnected(int n, int[][] connections) {
        int[] id = new int[n];
        for (int i = 0; i < n; i++) {
            id[i] = i;
        }
        int cons = n, ress = 0;
        for (int[] e : connections) {
            int p1 = find(id, e[0]);
            int p2 = find(id, e[1]);
            if (p1 == p2) {
                ress++;
            } else {
                cons--;
                id[p1] = p2;
            }
        }
        
        return ress + 1 >= cons ? cons - 1 : -1;
    }

    private int find(int[] id, int p) {
        if (p == id[p]) return p;
        id[p] = find(id, id[p]);
        return id[p];
    }
}

省份数量★★

547. 省份数量

题目】有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

提示

  • 1 <= n <= 200
  • n == isConnected.length
  • n == isConnected[i].length
  • isConnected[i][j] 为 1 或 0
  • isConnected[i][i] == 1
  • isConnected[i][j] == isConnected[j][i]

示例

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
-----------------------------------------------------
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

解题思路

class Solution {
    int[] id;
    int count = 0;
    
    public int findCircleNum(int[][] M) {
        if (M == null || M.length == 0 || M[0].length == 0) return 0;
        count = M.length;
        id = new int[M.length];
        for (int i = 0; i < M.length; i++) id[i] = i;
        for (int i = 0; i < M.length; i++) {
            for (int j = 0; j < M.length; j++) {
                if (M[i][j] == 1) union(i, j);   
            }
        }
        return count;
    }
    
    private int find(int p) {
        if (p == id[p]) return p;
        id[p] = find(id[p]);
        return id[p];
    }
    
    private void union(int p, int q) {
        int pid = find(p);
        int qid = find(q);
        if (pid == qid) return;
        id[pid] = qid;
        count--;
    }
}

岛屿数量★★

200. 岛屿数量

题目】给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

提示

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0' 或 '1'

示例

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
------------------------------------------------
输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

解题思路

方法一:并查集,只考虑右边和下边即可

class Solution {
    int[] id;

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        int R = grid.length, C = grid[0].length;
        int count = R * C;
        id = new int[R * C];
        for (int i = 0; i < id.length; i++) id[i] = i;

        for (int i = 0; i < R; i++) {
            for (int j = 0; j < C; j++) {
                char c = grid[i][j];
                int k = i * C + j;
                if (c == '0') {
                    count--;
                } else {
                    if (j < C - 1 && grid[i][j + 1] == '1') {
                        count += union(k, k + 1);
                    }
                    if (i < R - 1 && grid[i + 1][j] == '1') {
                        count += union(k, k + C);
                    }
                }
            }
        }

        return count;
    }

    private int find(int p) {
        if (p == id[p]) {
            return p;
        }
        id[p] = find(id[p]);
        return id[p];
    }

    private int union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot != qRoot) {
            id[pRoot] = qRoot;
            return -1;
        } else {
            return 0;
        }
    }

}

方法二:dfs,下沉与它连接的所有岛屿

class Solution {
    public int numIslands(char[][] grid) {
        int count = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                if (grid[i][j] == '1') {
                    sinkLand(grid, i, j);
                    count++;
                }
            }
        }
        return count;
    }

    public void sinkLand(char[][] grid, int r, int c){
        if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] == '0') {
            return;
        }
        grid[r][c] = '0';
        sinkLand(grid, r, c - 1);
        sinkLand(grid, r, c + 1);
        sinkLand(grid, r - 1, c);
        sinkLand(grid, r + 1, c);
    }
}

由斜杠划分区域★★

959. 由斜杠划分区域

题目】在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\空格构成。这些字符会将方块划分为一些共边的区域。

(请注意,反斜杠字符是转义的,因此 \"\\" 表示。)。

返回区域的数目。

提示:

  1. 1 <= grid.length == grid[0].length <= 30
  2. grid[i][j]'/''\'、或 ' '

示例

在这里插入图片描述

输入:
[
  " /",
  "  "
]
输出:1

在这里插入图片描述

输入:
[
  "//",
  "/ "
]
输出:3

解题思路

在这里插入图片描述

连接时需要考虑以下5种情况:

  • 字符为空格“ ”:此时需连接区域[1, 2, 3, 4]
  • 字符为斜杠"/":此时需连接区域[0, 3], [1, 2]
  • 字符为反斜杠"/":此时需连接区域[0, 1],[2, 3]
  • 考虑方格右端(如图中黄色区域所示)
  • 考虑方格下端(如图中青色区域所示)
class Solution {
    public int regionsBySlashes(String[] grid) {
        int N = grid.length;
        int size = 4 * N * N;
        UF uf = new UF(size);

        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                int k = i * N * 4 + j * 4;
                if (grid[i].charAt(j) == ' ') {
                    uf.union(k, k + 1);
                    uf.union(k + 1, k + 2);
                    uf.union(k + 2, k + 3);
                } else if (grid[i].charAt(j) == '/') {
                    uf.union(k, k + 3);
                    uf.union(k + 1, k + 2);
                } else {
                    uf.union(k, k + 1);
                    uf.union(k + 2, k + 3);
                }
                if (j < N - 1) {
                    uf.union(k + 1, k + 4 + 3);
                }
                if (i < N - 1) {
                    uf.union(k + 2, k + N * 4);
                }
            }
        }

        return uf.getCount();
    }

    class UF {
        int N;
        int count;
        int[] id;
        int[] sz;

        UF (int N) {
            this.N = N;
            count = N;
            id = new int[N];
            sz = new int[N];

            for (int i = 0; i < N; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount() {
            return count;
        }

        public void union (int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot != qRoot) {
                if (sz[pRoot] < sz[qRoot]) {
                    id[pRoot] = id[qRoot];
                    sz[qRoot] += sz[pRoot];
                } else {
                    id[qRoot] = id[pRoot];
                    sz[pRoot] += sz[qRoot];
                }
                count--;
            }
        }

        private int find (int p) {
            if (p == id[p]) return p;
            id[p] = find(id[p]);
            return id[p];
        }
    }
}

打砖块★★★

803. 打砖块

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

  • 一块砖直接连接到网格的顶部,或者
  • 至少有一块相邻(4 个方向之一)砖块 稳定 不会掉落时

给你一个数组 hits ,这是需要依次消除砖块的位置。每当消除 hits[i] = (rowi, coli) 位置上的砖块时,对应位置的砖块(若存在)会消失,然后其他的砖块可能因为这一消除操作而掉落。一旦砖块掉落,它会立即从网格中消失(即,它不会落在其他稳定的砖块上)。

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

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

提示

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • grid[i][j] 为 0 或 1
  • 1 <= hits.length <= 4 * 104
  • hits[i].length == 2
  • 0 <= xi <= m - 1
  • 0 <= yi <= n - 1
  • 所有 (xi, yi) 互不相同

示例

输入: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]--------------------------------------------------------------------------------
输入: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]

解题思路

此题考察的是逆向思维,并查集和DFS都可以完成。在此我使用并查集。

这题只需求每次打击位置后掉落的砖块数目即可,因此可将矩阵中第一行(最顶端位置的砖块)汇聚到根节点,逆向求解是,先使用一个临时副本矩阵,对此进行操作,将打击掉后的砖块状态存入并查集中,然后逆向求解每次将砖块补齐后,前后根节点连接子树的大小差值。

在这里插入图片描述

详细见官方题解打砖块

class Solution {
    public int[] hitBricks(int[][] grid, int[][] hits) {
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return new int[]{};
        }
        int R = grid.length, C = grid[0].length;
        int[][] copy = new int[R][C];
        for (int i = 0; i < R; i++) {
            for (int j = 0; j < C; j++) {
                copy[i][j] = grid[i][j];
            }
        }
        
        //1.击碎
        for (int[] hit : hits) {
            copy[hit[0]][hit[1]] = 0;
        }

        //2.建图连接
        int size = R * C;
        UF uf = new UF(size + 1);
        //顶层初始化
        for (int i = 0; i < C; i++) {
            if (copy[0][i] == 1) {
                uf.union(size, i);
            }
        }

        for (int i = 1; i < R; i++) {
            for (int j = 0; j < C; j++) {
                if (copy[i][j] == 1) {
                    int con = i * C + j;
                    //看上边
                    if (copy[i - 1][j] == 1) {
                        uf.union(con, con - C);
                    }
                    //看左边
                    if (j > 0 && copy[i][j - 1] == 1) {
                        uf.union(con, con - 1);
                    }
                }
            }
        }

        //3.逆序补回砖块
        int[] dirs = {-1, 0, 1, 0, -1};
        int len = hits.length;
        int[] res = new int[len];
        for (int i = len - 1; i >= 0; i--) {
            int x = hits[i][0], y = hits[i][1];
            int loc = x * C + y;
            if (grid[x][y] == 0) {
                continue;
            }
            int origin = uf.getSize(size);
            if (x == 0) {
                uf.union(size, loc);
            }
            //观察四个方向,将连接的网格合并
            for (int k = 0; k < 4; k++) {
                int newx = x + dirs[k], newy = y + dirs[k + 1];
                if (newx < 0 || newx >= R || newy < 0 || newy >= C) {
                    continue;
                }
                int newloc = newx * C + newy;
                if (copy[newx][newy] == 1) {
                    uf.union(loc, newloc);
                }
            }

            int current = uf.getSize(size);
            res[i] = Math.max(0, current - origin - 1);
            //补上砖块
            copy[x][y] = 1;
        }

        return res;
    }

    private class UF {
        int[] id;
        int[] sz;

        UF (int n) {
            id = new int[n];
            sz = new int[n];
            for (int i = 0; i < n; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getSize(int p) {
            int root = find(p);
            return sz[root];
        }

        public int find(int p) {
            if (p != id[p]) {
                id[p] = find(id[p]);
            }
            return id[p];
        }

        public void union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) return;
            id[qRoot] = pRoot;
            sz[pRoot] += sz[qRoot];
        }
    }
}

保证图可完全遍历★★★

1579. 保证图可完全遍历

题目AliceBob 共有一个无向图,其中包含 n 个节点和 3 种类型的边:

  • 类型 1:只能由 Alice 遍历。
  • 类型 2:只能由 Bob 遍历。
  • 类型 3:Alice 和 Bob 都可以遍历。

给你一个数组 edges ,其中 edges[i] = [typei, ui, vi] 表示节点 uivi 之间存在类型为 typei 的双向边。请你在保证图仍能够被 Alice和 Bob 完全遍历的前提下,找出可以删除的最大边数。如果从任何节点开始,Alice 和 Bob 都可以到达所有其他节点,则认为图是可以完全遍历的。

返回可以删除的最大边数,如果 Alice 和 Bob 无法完全遍历图,则返回 -1

提示

  • 1 <= n <= 10^5
  • 1 <= edges.length <= min(10^5, 3 * n * (n-1) / 2)
  • edges[i].length == 3
  • 1 <= edges[i][0] <= 3
  • 1 <= edges[i][1] < edges[i][2] <= n
  • 所有元组 (typei, ui, vi) 互不相同

示例

在这里插入图片描述

输入:n = 4, edges = [[3,1,2],[3,2,3],[1,1,3],[1,2,4],[1,1,2],[2,3,4]]
输出:2
解释:如果删除 [1,1,2][1,1,3] 这两条边,Alice 和 Bob 仍然可以完全遍历这个图。
再删除任何其他的边都无法保证图可以完全遍历。所以可以删除的最大边数是 2

解题思路

贪心思路,优先处理公共边,将公共边信息保存在并查集中,并计算多余的公共边(处于同一个连通分量);

然后对Alice和Bob分别处理(注意此时应在各自的并查集中执行操作),因为图可完全遍历等价于图中只存在一个连通分量,此时对其各自操作,边的顺序并不影响结果,累加多余的边(处于同一个连通分量)。

最后判断能否完全遍历即可。

class Solution {
    public int maxNumEdgesToRemove(int n, int[][] edges) {
        for (int[] edge : edges) {
            edge[1]--;
            edge[2]--;
        }
        UF ufa = new UF(n);
        UF ufb = new UF(n);
        int res = 0;

        //优先处理公共边
        for (int[] edge : edges) {
            if (edge[0] == 3) {
                res += ufa.union(edge[1], edge[2]);
                ufb.union(edge[1], edge[2]);
            }
        }
        //分别处理Alice和Bob的边
        for (int[] edge : edges) {
            if (edge[0] == 1) {
                res += ufa.union(edge[1], edge[2]);
            } else if (edge[0] == 2) {
                res += ufb.union(edge[1], edge[2]);
            }
        }

        return ufa.getCount() == 1 && ufb.getCount() == 1 ? res : -1;
    }

    private class UF {
        int count;   //连通分量个数
        int[] id;
        int[] sz;

        public UF(int n) {
            count = n;
            id = new int[n];
            sz = new int[n];
            for (int i = 0; i < n; i++) {
                id[i] = i;
                sz[i] = 1;
            }
        }

        public int getCount() {
            return count;
        }

        public int find(int p) {
            if (p != id[p]) {
                id[p] = find(id[p]);
            }
            return id[p];
        }

        public int union(int p, int q) {
            int pRoot = find(p);
            int qRoot = find(q);
            if (pRoot == qRoot) {
                return  1;
            }
            if (sz[pRoot] > sz[qRoot]) {
                id[qRoot] = pRoot;
                sz[pRoot] += sz[qRoot];
            } else {
                id[pRoot] = qRoot;
                sz[qRoot] += sz[pRoot];
            }
            count--;
            return 0;
        }
    }
}

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 27
    评论
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值