并查集 - 分析与总结
参考: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;
}
}