并查集算法 详解

时间:2022-07-24
本文章向大家介绍并查集算法 详解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

并查集的思想

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

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

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

并查集的构建

/*
      并查集是一个数组,每一个值保存一个父节点,形成一个集合,
      集合需要关注两个操作,查找父节点,合并
     */
    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;
    }