边刷题边理解—并查集

186 阅读4分钟

并查集

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。

并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。常常在使用中以森林来表示。

并查集解决的问题:给你一个集合,然后根据一定条件给这些集合分类,分类后的集合可以想象成一棵树。

  • 1.让我们快速判断某个元素在哪个集合里(哪棵树里),达到快速判断两个节点在不在同一颗树里。
  • 2.让我们合并两棵树

并查集维护一个数组parentparent数组中维护的不是元素本身,而是元素的下标索引,当然,这个下标索引是指向该元素的父元素的。

优化

  • 让一颗树里的所有节点都指向根节点(因为并查集的主要目的不是判断谁是谁的父亲,谁是谁的儿子,而是判断我俩是不是一家人,是不是有共同的祖先),让所有节点都指向根节点,两个节点只要快速判断一下父亲节点相不相同就能判断在不在一家,否则还要逐层向上找。【参考最近公共祖先LCA】
  • 合并的时候矮的树往高的树上合并,防止树太深,到时候找祖先不好找。

所以我们需要维护两个数组:

  • parent数组,例:parent[1]的值为2 就代表节点1的父亲节点是2
  • rank 数组,例:rank[5]=3,代表以5为

所以,并查集有两个重要的功能

  • 查找祖先【根节点】

    	static int N;
        static int fa[]=new int[N];
        int find(int x){
            if (x!=fa[x])//x不是自身的父亲
                fa[x]=find(fa[x]);
            return fa[x];
    
        }
    
  • 合并两棵树

    维护rank数组是一件很麻烦的事情,为了更好得理解,这里先选择不写

    void unionSet(int x,int y){
            x=find(x);
            y=find(y);
            fa[x]=fa[y];
        }
    

例题:

【LeetCode】联通网络中的操作次数

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

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

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

示例 1:

img

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

示例 2:

img

输入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]]
输出:2

示例 3:

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

示例 4:

输入:n = 5, connections = [[0,1],[0,2],[3,4],[2,3]]
输出:0

分析

  • n台机器互联,需要n-1条线
  • N个集合互联,需要n-1条线
  • 1.首先判断有几个集合,确定连接这些集合需要多少根连接线
  • 2.确定每个集合冗余多少根连接线
  • 3.若冗余数量小于需要数量返回-1,否则返回需要数量

代码


/**
 * @author SJ
 * @date 2021/4/7
 */
public class MakeConnect {
    public int N;//机器数量
    public int[] fa;//父节点
    public int count;//集合数量
    public int lineCanBeReuse=0;//冗余连接线数量

    //初始化
    public void initialize(int n){
        N = n;
        count = N;
        fa = new int[N];
        for (int i = 0; i < N; i++) {
            fa[i] = i;
        }
    }

    //路径压缩
    public int find(int x) {
        if (x != fa[x])
            //如果x不是自身的父亲,证明x不是老祖宗,就沿着他爹向上找
            x = find(fa[x]);
        return fa[x];
    }


    //合并
    public void merge(int x, int y) {
        x = find(x);
        y = find(y);
        //每合并两棵树,集合数量少1
        if (x != y) {
            fa[x] = fa[y];
            count--;

        }else //如果这两颗树祖先相同,但他们还有连接线相连,证明该条连接线冗余。
            lineCanBeReuse++;
    }

    public int makeConnected(int n, int[][] connections) {
        initialize(n);

        for (int[] connection : connections) {
            merge(connection[0], connection[1]);
        }
        if (lineCanBeReuse<count-1)
            return -1;
        else
            return count-1;


    }

    public static void main(String[] args) {
        int[][] c={{0,1},{0,2},{1,2}};
        MakeConnect makeConnect = new MakeConnect();
        int i = makeConnect.MakeConnected(4, c);
        System.out.println(i);

    }

}

总结:并查集的代码写好之后,你只要遍历每个点,然后合并就完事了

【LeetCode】岛屿数量

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

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

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

示例 1:

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

分析

首先,那道题我们有两种思路。

  • 第一种,使用图的搜索。

    • 对于每一个值为1的点,进入图的dfs,将可达【根据题意水平或竖直相邻】的点的值设为0. 直至没有可达的点。这就搜完了第一个岛屿。剩下的岛屿也就这么搜就完事了。
  • 第二种,使用并查集。

    • 如果一个位置为 1,则将其与相邻四个方向上的 1 在并查集中进行合并。最终岛屿的数量就是并查集中连通分量的数目。

今日并查集专场。:smile:

代码

import java.time.YearMonth;

/**
 * @author SJ
 * @date 2021/4/7
 */
public class NumIsland {

    //先上一套并查集
    public int N;
    public Integer[] fa;//存祖先,考虑到有不少不相关的点,我们建立Integer 可以存0值
    public int count;//存岛屿数量


    //初始化来一套
    public void initialize(int n) {
        N = n;
        fa = new Integer[N];
        count = 0;

    }

    //查找来一套
    public int find(int x) {
        if (x != fa[x])
            fa[x] = find(fa[x]);
        return fa[x];
    }

    //合并来一套
    public void merge(int x, int y) {
        x = find(x);
        y = find(y);
        if (x != y) {
            fa[x] = fa[y];
            count--;

        }

    }

    //开始合并啦
    public int numIslands(char[][] grid) {

        int Y = grid.length;
        int X = grid[0].length;
        initialize(X * Y);
        for (int i = 0; i < Y; i++) {
            for (int j = 0; j < X; j++) {
                if (grid[i][j] == '1') {
                    fa[i * X + j] = i * X + j;
                    count++;
                }

            }
        }

        int[][] state = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}};
        int curI = 0;
        int curJ = 0;

        //我们将点【i,j】表示i*X+j
        for (int i = 0; i < Y; i++) {
            for (int j = 0; j < X; j++) {
                if (grid[i][j] == '1') {
                    grid[i][j] = '0';

                    for (int[] ints : state) {
                        curI = i + ints[0];
                        curJ = j + ints[1];

              //不吹不黑,因为每1个点都要遍历周围四个点,导致并查集的效率一般,有很大的优化空间,还不如深搜呢,深搜起码还有状态标记
                        if (curI >= 0 && curI < Y && curJ >= 0 && curJ < X && grid[curI][curJ] == '1') {
                            merge(i * X + j, curI * X + curJ);

                        }
                    }
                }
            }
        }
        return count;

    }

    public static void main(String[] args) {
        char[][] a = {{'1', '1', '1', '1', '0'}, {'1', '1', '0', '1', '0'}, {'1', '1', '0', '0', '0'}, {'0', '0', '0', '0', '0'}};
        NumIsland numIsland = new NumIsland();
        int i = numIsland.numIslands(a);
        System.out.println(i);
    }


}