图算法 - 并查集

241 阅读3分钟

并查集 - 分析与总结


参考:labuladong的算法小抄

时间:2022年10月31日

整理:alec_97


引言

并查集主要是解决图论中的动态连通性问题。

一、问题介绍

动态连通性:

AB相连,BC相连,那么AC也相连。

并查集主要实现两个API,并和查:

并即将两个点联通,查即判断两个点是否相连

二、基本思路

抽象:

用森林表示图的动态连通性

具体:

使用数组来具体实现这个森林

并的实现过程:

如果两个节点被联通,则让其中一个节点的根节点连接到另一个节点的根节点上,同时联通分量的个数减一

查的实现过程:

如果两个节点联通的话,则一定有相同的根节点。

且寻找根节点的过程,可以通过递归来实现。

时间复杂度:

并和查两个主要的API的时间复杂度,主要是find函数主导的。

find的复杂度主要是树的复杂度。

平衡情况下是O(logN),极端情况下是退化成链表变成了O(N)

三、平衡性优化

主要是在并的时候可能发生不平衡的现象,因此平衡性优化主要是修改的逻辑

解决方案是,利用一个数组,记录以每个节点为根的树,重量是多少。重量即为该树上的节点的数量。

同时在连接的时候,将轻的树连接到重的树上面。

通过这种方式能够极大的优化树的平衡程度,使得树接近O(logN)

四、路径压缩

这步优化、代码简单、但是原理很巧妙。路径压缩主要是在 find 逻辑即 查 的过程中实现的。

我们不在乎树长什么样,因为并和查的关键点都是根节点。(其中并是将根节点相连,查则是判断根节点是否是同一个。)

因此能够进一步压缩树的高度,使之保持为常数;做到这一点主要是修改find函数的逻辑。

路径压缩,主要有两种优化方式:

方式1:

在 find 根节点的过程中,将当前节点指向当前节点的爷爷节点,从而实现路径压缩。

实现方式为将a->b->c修改为a->c, b->c

private int find(int x) {
    while (parent[x] != x) {
        // 这行代码进行路径压缩
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;
}

方式2:

在 find 根节点的过程中,将每个节点都指向唯一的根节点, 这种方式极大的优化了效率,将 O(N) 的或者 O(logN) 的复杂度优化为了 O(1) 的复杂度。

实现方式为使用递归的方式,在递归函数中将每层的节点指向根节点。

// 第二种路径压缩的 find 方法
public int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);
    }
    return parent[x];
}

其中,路径压缩的优化方式2 直接将复杂度优化为常数级别,因为也就不需要进行平衡性的优化。因此常见的并查集模板中,直接使用的路径压缩方式2进行优化,没有使用平衡度优化和路径压缩方式1优化。

五、union find 算法模板

class UF {
    // 连通分量个数
    private int count;
    // 存储每个节点的父节点
    private int[] parent;

    // n 为图中节点的个数
    public UF(int n) {
        this.count = n;
        parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }
    
    // 将节点 p 和节点 q 连通
    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        
        if (rootP == rootQ)
            return;
        
        parent[rootQ] = rootP;
        // 两个连通分量合并成一个连通分量
        count--;
    }

    // 判断节点 p 和节点 q 是否连通
    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }

    // 核心优化逻辑:路径压缩方式2
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    // 返回图中的连通分量个数
    public int count() {
        return count;
    }
}