一. 并查集的介绍

1.并查集的简单介绍

        并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。并查集跟树有些类似,只不过她跟树是相反的。在树这个数据结构里面,每个节点会记录它的子节点。在并查集里,每个节点会记录它的父节点【1】。

        并查集的精髓在于,两块集合(区域)有没有相交,以及要不要联通两块区域(集合)。参考树的结构,把元素挂到父节点下,若两个元素的父节点相同,则是同一块区域,两个元素的父节点不相同,则不是同一块区域。

并查集python 并查集 java_父节点

        以上图为例,1和2是同一片区域,3和4是同一片区域,5自己是一片区域,那么以上总共有三个区域。

2.并查集的典型使用场景

(1)最大岛屿面积(leetcode695)

并查集python 并查集 java_父节点_02

 0代表水,1代表陆地,求最大的岛屿数量(斜方向不算相交),那么图中最大面积是6(橘黄色)

(2)岛的数量(leetcode200)

并查集python 并查集 java_散列表_03

 这是并查集的典型使用场景,求连通分量,和(1)差不多,但是更简单

(3)省份数量(leetcode547)

并查集python 并查集 java_父节点_04

并查集python 并查集 java_数据结构_05

在左图中,城市1和城市2是联通的,所以左图共有两个省份。

在右图中,城市和城市都不是联通的,所以共有三个省份。

二. 并查集的主要构成和实现方式

1. 主要构成

(0)总体构成

  • 并,代表合并
  • 查,代表查找
  • 集,代表一个集合,这个集合中,大家的父节点都指向一个地方,即每个节点会记录它的父节点

(1)数据结构实现(HashMap版本)

让子节点指向父节点,可以用HashMap实现,即让Key指向value。

class UnionFind {
    private Map<Integer,Integer> father;
}

 加入father现在有三个entry,1->2,2->3,4->5.那么它的图示是这样子的:

并查集python 并查集 java_散列表_06

(2) 元素添加的过程

以用HashMap的方式实现为例,那么添加一个新的元素时,它的父节点是null

public void add(int x){
        if(!father.containsKey(x)){//从来没有在father中出现过
            father.put(x,null);
            numOfSets++;//联通区域需要加1
        }
    }

 添加一个新的元素时,它的父节点也可以是它自己。

public void add(int x){
        if(!father.containsKey(x)){//从来没有在father中出现过
            father.put(x,x);
        }
    }

这两种方式都是可以的,下面我们以第一种为例。

添加过程:

并查集python 并查集 java_并查集python_07

 在刚开始不发生合并等操作时,自己都是自己的父节点。

(3)查找父节点过程find()

public int find(int x) {
        int root = x;
        while(father.get(root) != null){
            root = father.get(root);
        }
         
        return root;
    }

 层层向上查找,那么找到父节点是null的哪个节点就是祖先了。

并查集python 并查集 java_并查集python_08

(4)缩减层数的过程

这里有一个优化的点:如果我们树很深,比如说退化成链表,那么每次查询的效率都会非常低。所以我们要做一下路径压缩。也就是把树的深度固定为二。【1】

这么做可行的原因是,并查集只是记录了节点之间的连通关系,而节点相互连通只需要有一个相同的祖先就可以了。【1】

把所有是同父节点的节点进行一路压缩,可以减少查询的次数,基本上可变成O(1)的查询时间。

方法一. 层层递归向上,写个while循环或者递归就可以了【1】

public int find(int x) {
        int root = x;
        
        while(father.get(root) != null){
            root = father.get(root);
        }
        
        while(x != root){
            int original_father = father.get(x);
            father.put(x,root);
            x = original_father;
        }
        
        return root;
    }

方法二. 一路压到栈中,然后层层出栈,把沿路的都挂到父节点上【3】

Stack<Element<V>> path = new Stack<>();
            while (element != fatherMap.get(element)) {
                path.push(element);
                element = fatherMap.get(element);
            }
            while (!path.isEmpty()) {
                fatherMap.put(path.pop(), element);
            }

  以(3)中图为例,那么压缩路径的图示如下:

并查集python 并查集 java_图论_09

 

(5)并的过程merge()

merge的本质是把两个父节点不同的节点进行合并,从而让他们的父节点相同。

public void merge(int x,int y){
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            father.put(rootX,rootY);
            numOfSets--;
        }
    }

 以(1)中为例,那么图示为:

并查集python 并查集 java_父节点_10

 

2. HashMap实现模板Ref.【3】

模板代码

public class Code04_UnionFind {

    public static class Element<V> {
        public V value;

        public Element(V value) {
            this.value = value;
        }

    }

    public static class UnionFindSet<V> {
        public HashMap<V, Element<V>> elementMap;
        public HashMap<Element<V>, Element<V>> fatherMap;
        public HashMap<Element<V>, Integer> rankMap;

        public UnionFindSet(List<V> list) {
            elementMap = new HashMap<>();
            fatherMap = new HashMap<>();
            rankMap = new HashMap<>();
            for (V value : list) {
                Element<V> element = new Element<V>(value);
                elementMap.put(value, element);
                fatherMap.put(element, element);
                rankMap.put(element, 1);
            }
        }

        private Element<V> findHead(Element<V> element) {
            Stack<Element<V>> path = new Stack<>();
            while (element != fatherMap.get(element)) {
                path.push(element);
                element = fatherMap.get(element);
            }
            while (!path.isEmpty()) {
                fatherMap.put(path.pop(), element);
            }
            return element;
        }

        public boolean isSameSet(V a, V b) {
            if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
                return findHead(elementMap.get(a)) == findHead(elementMap.get(b));
            }
            return false;
        }

        public void union(V a, V b) {
            if (elementMap.containsKey(a) && elementMap.containsKey(b)) {
                Element<V> aF = findHead(elementMap.get(a));
                Element<V> bF = findHead(elementMap.get(b));
                if (aF != bF) {
                    Element<V> big = rankMap.get(aF) >= rankMap.get(bF) ? aF : bF;
                    Element<V> small = big == aF ? bF : aF;
                    fatherMap.put(small, big);
                    rankMap.put(big, rankMap.get(aF) + rankMap.get(bF));
                    rankMap.remove(small);
                }
            }
        }

    }

}

3. HashMap实现 Ref.【1】

模板代码

class UnionFind {
    private Map<Integer,Integer> father;
    
    public UnionFind() {
        father = new HashMap<Integer,Integer>();
    }
    
    public void add(int x) {
        if (!father.containsKey(x)) {
            father.put(x, null);
        }
    }
    
    public void merge(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX != rootY){
            father.put(rootX,rootY);
        }
    }
    
    public int find(int x) {
        int root = x;
        
        while(father.get(root) != null){
            root = father.get(root);
        }
        
        while(x != root){
            int original_father = father.get(x);
            father.put(x,root);
            x = original_father;
        }
        
        return root;
    }
    
    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
} 

作者:musiala
链接:https://leetcode-cn.com/problems/number-of-provinces/solution/python-duo-tu-xiang-jie-bing-cha-ji-by-m-vjdr/

 4. 数组实现 Ref.【2】

// 并查集模板
    public class UnionFind {
        private int[] parent; // 存储一棵树 记录每个节点的父节点 相当于指向父节点的指针
        private int count;// 连通分量总数

        // Constructor 初始化
        public UnionFind(int n) {
            parent = new int[n];
            count = n;
            for (int i = 0; i < n; i++) {
                // 初始化每个节点都指向自己,没有其他连通
                parent[i] = i;
            }
        }

        // find , union, isConnected, count
        public int find(int x) {
            while (x != parent[x]) {
                // 压缩路径 优化
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }

        public void union(int x, int y) {
            if (find(x) == find(y)) {
                return;
            }
            // x的根节点 连接 y. 这里没有优化树的平衡,即小树接大树
            parent[find(x)] = find(y);
            // 合并连接后,连通分量总数减少
            count--;
        }
        // 判断 x&y 是否连通
        public boolean isConnected(int x, int y) {
            return find(x) == find(y);
        }

        public int getCount() {
            // 返回当前的连通分量个数
            return count;
        }
    }

作者:weichen-wang
链接:https://leetcode.cn/problems/number-of-islands/solution/dfs-bfs-unionfindbing-cha-ji-by-weichen-7ldae/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

三. leetcode实战

leetcode547

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量.

class Solution {
    public int findCircleNum(int[][] isConnected) {
      UnionFind uf = new UnionFind();
      for(int i = 0; i < isConnected.length;i++){
          uf.add(i);
          for(int j = 0; j < i; j++){
              if(isConnected[i][j] == 1){
                  uf.merge(i,j);
              }
          }
      }
      return uf.numOfSets;
    }
}

//首先我们需要一个class结构,来实现并查集结构
class UnionFind{
    //HashMap存放每个结点和它的父节点
    HashMap<Integer,Integer> father;
    //存放联通的区域
    int numOfSets = 0;
    //初始化
    public UnionFind(){
        father = new HashMap<>();
        numOfSets = 0;
    }
    //添加结点的过程
    public void add(int x){
        if(!father.containsKey(x)){//从来没有在father中出现过
            father.put(x,null);
            numOfSets++;//联通区域需要加1
        }
    }
    //查找的过程
    public int find(int x){
        int root = x;
        while(father.get(root) != null){//在father中一致循环找到它的父节点,层层往上
            root = father.get(root);
        }
        //减层的操作
        while(x != root){
            int original_father = father.get(x);//先保留父节点
            father.put(x,root);//把x直接挂到父节点下
            x = original_father;//x变成原来的父节点,然后循环
        }
        return root;
    }

    //把两个结点合成一个结点,若父节点一样则不同合了
    public void merge(int x,int y){
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            father.put(rootX,rootY);
            numOfSets--;
        }
    }

    //x和y是否是联通的,找有没有共同的父节点就可以了
    public boolean isConnected(int x, int y){
        return find(x) == find(y);
    }

    //返回联通区域的数量
    public int getNumOfSets(){
        return numOfSets;
    }
}

leetcode684

树可以看成是一个连通且 无环 的 无向 图。

给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。

class UnionFind{
    HashMap<Integer,Integer> father;
    int numset;
    public UnionFind(){
        numset = 0;
        father = new HashMap<>();
    }
    public void add(int x){
        if(!father.containsKey(x)){
            father.put(x,null);
            numset++;
        } 
    }
    public int find(int x){
        int root = x;
        while(father.get(root) != null){
            root = father.get(root);
        }
        //压缩
        while(father.get(x) != null){ 
            int ori_father = father.get(x);
            father.put(x,root);
            x = ori_father;
        }
        return root;
    }

    public void merge(int x, int y){
        int rootX = find(x);
        int rootY = find(y);
        if(rootX != rootY){
            father.put(rootX,rootY);
            numset--;
        }
    }

    public boolean isConnected(int x, int y){
        return find(x) == find(y);
    }

    public int getnumset(){
        return numset;
    }
}

class Solution {
    public int[] findRedundantConnection(int[][] edges) {
       UnionFind uf = new UnionFind();
       for(int i = 0; i < edges.length; i++){
           if(uf.isConnected(edges[i][0],edges[i][1])){
               return edges[i];
           }
           else{
               uf.add(edges[i][0]);
               uf.add(edges[i][1]);
               uf.merge(edges[i][0],edges[i][1]);
           }
       }
       return edges[0];
    }
}