并查集算法 详解

作者: Tim在路上 | 来源:发表于2020-08-31 11:57 被阅读0次

    并查集的思想

    并查集是一种树形的数据结构,用于处理不相交集的合并查询,一般具有两个基本的操作,查找确定元素在哪一个子集,合并将两个子集进行合并。

    并查集的两个优化是路径压缩和启发式的合并。

    金典应用是最小生成树和最大公共祖先,检测图是否有环。

    并查集的构建

    /*
          并查集是一个数组,每一个值保存一个父节点,形成一个集合,
          集合需要关注两个操作,查找父节点,合并
         */
        public int find(int[] parents, int d){
            int f_root = d;
            while (parents[f_root] != -1){
                d = parents[f_root];
            }
            return f_root;
        }
        
        // 合并集合,合并集合最简单的操作是将其父节点进行关联
        public int union(int[] parents, int x , int y){
            int x_root = find(parents,x);
            int y_root = find(parents,y);
            if(x_root == y_root){
                return 0;
            }
            parents[x_root] = y_root;
            return 1;
        }
    

    第二种方式作为内部类进行使用

    static class DUS{
            int[] parent;
            public DUS(int n){
                parent = new int[n];
                for(int i=0;i<n;i++){
                    parent[i] = i;
                }
            }
            public int find(int x){
                if(parent[x] != x){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if (x_root == y_root){
                    return 0;
                }
                if (x_root < y_root){
                    parent[y_root] = x_root;
                }else {
                    parent[x_root] = y_root;
                }
                return 1;
            }
        }
    

    // 三种启发式合并,避免形成的树过深

       class DSU {
            int[] parent;
            int[] rank;
            int N = 6;
    
            public DSU() {
                parent = new int[N];
                rank = new int[N];
                for(int i=0;i<N;i++){
                    parent[i] = i;
                }
            }
    
            // 这里顺便做路径压缩,将查到的父节点赋值给当前节点
            public int find(int x){
                if(parent[x] != x){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if(x_root == y_root){
                    return 0;
                }
                // 将大指向小进行统一
                // 使用启发式合并,避免形成的树过深
                if(rank[x_root] < rank[y_root]){
                    parent[x_root] = y_root;
                }else if (rank[x_root] > rank[y_root]){
                    parent[y_root] = x_root;
                }else{
                    rank[x_root]++;
                    parent[y_root] = x_root;
                }
                return 1;
            }
        }
    

    eg 题目

    判断图是否有环

    思路是,选择边,如果当前的边的点存在集合有加入对应集合,否则开辟新集合,如果存在两个集合有的将两个集合进行合并,如果加入的点都在一个集合里,说明存在环。

    1559. 二维网格图中探测环

    给你一个二维字符网格数组 grid ,大小为 m x n ,你需要检查 grid 中是否存在 相同值 形成的环。

    一个环是一条开始和结束于同一个格子的长度 大于等于 4 的路径。对于一个给定的格子,你可以移动到它上、下、左、右四个方向相邻的格子之一,可以移动的前提是这两个格子有 相同的值 。

    同时,你也不能回到上一次移动时所在的格子。比方说,环 (1, 1) -> (1, 2) -> (1, 1) 是不合法的,因为从 (1, 2) 移动到 (1, 1) 回到了上一次移动时的格子。

    如果 grid 中有相同值形成的环,请你返回 true ,否则返回 false 。

    输入:grid = [["a","a","a","a"],["a","b","b","a"],["a","b","b","a"],["a","a","a","a"]]

    输出:true

    class Solution {
         static class DUS{
            int[] parent;
            public DUS(int n){
                parent = new int[n];
                for(int i=0;i<n;i++){
                    parent[i] = -1;
                }
            }
            public int find(int x){
                while(parent[x] != -1){
                    x = parent[x];
                }
                return x;
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if (x_root == y_root){
                    return 0;
                }
                if (x_root < y_root){
                    parent[y_root] = x_root;
                }else {
                    parent[x_root] = y_root;
                }
                return 1;
            }
        }
    
        // 思路将二维数组当做一维数组,做并查集,判断存在环,将一个边的两个点加入并查集
        // 将一个边的两个点加入并查集
    
        public boolean containsCycle(char[][] grid) {
            int m = grid.length;
            int n = grid[0].length;
            DUS dus = new DUS(m*n);
            for(int i=0;i<m;i++){
                for(int j=1;j<n;j++){
                    if(grid[i][j-1] == grid[i][j]){
                        int u = dus.union(i * n + j, i * n + j - 1);
                        if (u == 0){
                            return true;
                        }
                    }
                }
            }
            for(int i=0;i<n;i++){
                for(int j=1;j<m;j++){
                    if(grid[j-1][i] == grid[j][i]){
                        int u = dus.union((j-1) * n + i, j * n + i);
                        if (u == 0){
                            return true;
                        }
                    }
                }
            }
            return false;
        }
    }
    
    684. 冗余连接

    在本问题中, 树指的是一个连通且无环的无向图。

    输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。

    结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。

    返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。

    示例 1:

    输入: [[1,2], [1,3], [2,3]]
    输出: [2,3]

    class DUS {
            int[] parent;
            public DUS (int n){
                parent = new int[n+1];
                for (int i=1;i<=n;i++){
                    parent[i] = i;
                }
            }
    
            public int find(int x){
                if(parent[x] != x){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if (x_root == y_root){
                    return 0;
                }
                if (x_root < y_root){
                    parent[y_root] = x_root;
                }else {
                    parent[x_root] = y_root;
                }
                return 1;
            }
    
        }
    
    
        public  int[] findRedundantConnection(int[][] edges) {
            int n = edges.length;
            DUS dus = new DUS(n);
            for (int[] nums: edges){
                int i = dus.union(nums[0],nums[1]);
                if (i == 0){
                    return nums;
                }
            }
            return new int[0];
        }
    

    统计图形成联通集的个数

    思路在向并查集中添加完所有的边对应的点后,统计现在是顶点的个数,一个顶点是一个联通图,parent[i] = i

    包括统计每一个联通图里面节点的个数

    547 朋友圈

    班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。

    给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。

    示例 1:

    输入:
    [[1,1,0],
    [1,1,0],
    [0,0,1]]

    输出:2
    解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。
    第2个学生自己在一个朋友圈。所以返回 2 。

    class Solution {
    
        // 思路形成并查集,并将其所有节点都标记为父节点,统计父节点的个数
        // 初始让所有节点父节点是本身
    
        class DUS{
            int[] parent;
            public DUS(int n){
                parent = new int[n];
                for(int i=0;i<n;i++){
                    parent[i] = i;
                }
            }
    
            public int find(int x){
                if(parent[x] != x){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if(x_root == y_root){
                    return 0;
                }
                if(x_root < y_root){
                    parent[y_root] = x_root;
                }else{
                    parent[x_root] = y_root;
                }
                return 1;
            }
    
            public int getRootNum(){
                int count = 0;
                for(int i=0;i<parent.length;i++){
                    if(parent[i] == i){
                        count++;
                    }
                }
                return count;
            }
        }
    
        public int findCircleNum(int[][] M) {
            int n = M.length;
            DUS dus = new DUS(n);
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    if(M[i][j] == 1 && i != j){
                        dus.union(i,j);
                    }
                }
            }
            return dus.getRootNum();
        }
    }
    
    959. 由斜杠划分的区域

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

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

    返回区域的数目。

     public int regionsBySlashes(String[] grid) {
            int N = grid.length;
            DSU dsu = new DSU(4 * N * N);
            for (int r = 0; r < N; ++r)
                for (int c = 0; c < N; ++c) {
                    int root = 4 * (r * N + c);
                    char val = grid[r].charAt(c);
                    if (val != '\\') {
                        dsu.union(root + 0, root + 1);
                        dsu.union(root + 2, root + 3);
                    }
                    if (val != '/') {
                        dsu.union(root + 0, root + 2);
                        dsu.union(root + 1, root + 3);
                    }
    
                    // north south
                    if (r + 1 < N)
                        dsu.union(root + 3, (root + 4 * N) + 0);
                    if (r - 1 >= 0)
                        dsu.union(root + 0, (root - 4 * N) + 3);
                    // east west
                    if (c + 1 < N)
                        dsu.union(root + 2, (root + 4) + 1);
                    if (c - 1 >= 0)
                        dsu.union(root + 1, (root - 4) + 2);
                }
    
            int ans = 0;
            for (int x = 0; x < 4 * N * N; ++x) {
                if (dsu.find(x) == x)
                    ans++;
            }
    
            return ans;
        }
    }
    
    class DSU {
        int[] parent;
    
        public DSU(int N) {
            parent = new int[N];
            for (int i = 0; i < N; ++i)
                parent[i] = i;
        }
    
        public int find(int x) {
            if (parent[x] != x) parent[x] = find(parent[x]);
            return parent[x];
        }
    
        public void union(int x, int y) {
            parent[find(x)] = find(y);
        }
    }
    
    17.07 婴儿名字

    每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。

    在结果列表中,选择字典序最小的名字作为真实名字。

    示例:

    输入:names = ["John(15)","Jon(12)","Chris(13)","Kris(4)","Christopher(19)"], synonyms = ["(Jon,John)","(John,Johnny)","(Chris,Kris)","(Chris,Christopher)"]
    输出:["John(27)","Chris(36)"]

    static class DUS {
            int[] parent;
            public DUS (int n){
                parent = new int[n];
                for (int i=0;i<n;i++){
                    parent[i] = i;
                }
            }
    
            public int find(int x){
                if(parent[x] != x){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if (x_root == y_root){
                    return 0;
                }
                if (x_root < y_root){
                    parent[y_root] = x_root;
                }else {
                    parent[x_root] = y_root;
                }
                return 1;
            }
    
        }
    
    
        // 将孩子的名字编号,并将其次数统计出来
        public static String[] trulyMostPopular(String[] names, String[] synonyms) {
            List<String> nameList = new ArrayList<String>();
            HashMap<String,Integer> map = new HashMap<String, Integer>();
    
            for(String item: names){
                int idx = item.indexOf("(");
                String sub = item.substring(idx+1,item.length()-1);
                String name = item.substring(0,idx);
                nameList.add(name);
                map.put(name,Integer.parseInt(sub));
            }
            Collections.sort(nameList);
            DUS dus = new DUS(nameList.size());
            for (String pair: synonyms){
                String[] pairs = pair.substring(1,pair.length()-1).split(",");
                int x = nameList.indexOf(pairs[0]);
                int y = nameList.indexOf(pairs[1]);
                if (x== -1 || y == -1) continue;
                dus.union(x, y);
            }
            HashMap<String, Integer> res = new HashMap<String, Integer>();
            for (int i=0;i<nameList.size();i++){
                int root = dus.find(i);
                String originName = nameList.get(i);
                String rootName = nameList.get(root);
                res.put(rootName, res.getOrDefault(rootName,0) + map.get(originName));
            }
            List<Map.Entry<String,Integer>> list = new ArrayList<>(res.entrySet());
            String[] result = new String[res.size()];
            int idx = 0;
            for (Map.Entry<String,Integer> entry: list){
                result[idx++] = entry.getKey() + "(" + entry.getValue() + ")";
            }
            return result;
        }
    

    判断两个点是不是同一个联通集里面的

    判断两个点是不是同一个联通集里面的,先将形成联通集的加入形成最终的联通集,最后将判断节点依次判断父节点是否相同

    990. 等式方程的可满足性

    给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

    只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

    示例 1:

    输入:["a==b","b!=a"]
    输出:false
    解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。

        // 使用并查集,将小的作为父节点,
        class BUS {
            int[] parent;
            int N = 26;
            public BUS(){
                parent = new int[N];
                for(int i=0;i<N;i++){
                    parent[i] = i;
                }
            }
    
            public int find(int x){
                if(parent[x] != x){
                    parent[x] = find(parent[x]);
                }
                return parent[x];
            }
    
            public int union(int x, int y){
                int x_root = find(x);
                int y_root = find(y);
                if(x_root == y_root){
                    return 0;
                }
                if(x_root < y_root){
                    parent[y_root] = x_root;
                }else{
                    parent[x_root] = y_root;
                }
                return 1;
            }
        }
    
        public boolean equationsPossible(String[] equations) {
            BUS bus = new BUS();
            for(String item : equations){
                int x = item.charAt(0) - 'a';
                int y = item.charAt(3) - 'a';
                char c = item.charAt(1);
                if(c == '='){
                    bus.union(x,y);
                }
            }
            for(String item : equations){
                int x = item.charAt(0) - 'a';
                int y = item.charAt(3) - 'a';
                char c = item.charAt(1);
                if(c == '!'){
                int x_root = bus.find(x);
                int y_root = bus.find(y);
                if(x_root == y_root){
                    return false;
                }
                }
            }
            return true;
        }
    

    相关文章

      网友评论

        本文标题:并查集算法 详解

        本文链接:https://www.haomeiwen.com/subject/ltsesktx.html