并查集
并查集理论基础
并查集主要有两个功能:将两个元素添加到一个集合中。判断两个元素在不在同一个集合
思想:用一个一维数组表示三个元素A,B,C (分别是数字)在同一个集合,即father[A]=B,father[B]=C 表示A与B与C连通了(有向连通图)。但无法也无需知道B连通A,类似树的结构,把root当成分类的标准,当root都相等的时候,说明在同一个集合
怎么判断元素联通/元素在同一个集合,只需要递归求得元素的根是否是同一个,
怎么表示C也在同一个元素集合里呢? 只需 father[C] = C,即C的根也为C,所以father数组初始化的时候要 father[i] = i,默认自己指向自己。
路径压缩:为了避免在多叉树情况下寻根递归次数过多,将非根节点的所有节点直接指向根节点。只需要在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果。
并查集的模板
public class UnionFind {
private int[] father;
private int n;
// 构造函数初始化并查集
public UnionFind(int n) {
this.n = n;
father = new int[n];
init();
}
// 并查集初始化
private void init() {
for (int i = 0; i < n; i++) {
father[i] = i;
}
}
// 并查集里寻根的过程
public int find(int u) {
if (u != parent[u]) {
parent[u] = find(parent[u]); // 路径压缩,将当前节点指向根节点
}
return parent[u];
}
// 判断 u 和 v 是否属于同一个集合
public boolean isSame(int u, int v) {
return find(u) == find(v);
}
// 将 v -> u 这条边加入并查集
public void join(int u, int v) {
u = find(u); // 寻找 u 的根
v = find(v); // 寻找 v 的根
if (u != v) { // 如果不在同一个根,将 v 的根设置为u
father[v] = u;
}
}
// 测试方法
public static void main(String[] args) {
int n = 1005; // 假设有 1005 个节点
UnionFind uf = new UnionFind(n);
// 示例操作
uf.join(1, 2);
uf.join(2, 3);
System.out.println(uf.isSame(1, 3)); // 输出 true
System.out.println(uf.isSame(1, 4)); // 输出 false
uf.join(3, 4);
System.out.println(uf.isSame(1, 4)); // 输出 true
}
}
通过模板,我们可以知道,并查集主要有三个功能。
寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
- 常见误区
模板中的 join 函数里的这段代码:
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
与 isSame 函数的实现是不是重复了? 如果抽象一下呢,代码如下:
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
if (isSame(u, v)) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
![并查集误区.png](https://290ff162.telegraph-image-eg9.pages.dev/file/953ebc346de365dff82ea.png)
![并查集模拟.png](https://290ff162.telegraph-image-eg9.pages.dev/file/971ce42dd9541db0703a7.png)
![拓展.png](https://290ff162.telegraph-image-eg9.pages.dev/file/af4735f3484bd995c42be.png)
空间复杂度: O(n) ,申请一个father数组。
时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。
在第一次查询的时候,相当于是n叉树上从叶子节点到根节点的查询过程,时间复杂度是logn,
路径压缩后,后面的查询操作都是O(1),而 join 函数 和 isSame函数 里涉及的查询操作也是一样的过程。
使用场景
- Kruskal 最小生成树算法
- 网格渗透
- 网络连通性
- 树中的东方共同祖先
- 图像处理
复杂度
Construction O(n)
Union α(n)
Find α(n)
Get component size α(n)
Check if connected α(n)
Count components O(1)
α摊销固定时间