并查集(UnionFind)常常用于处理一些不相交集合的合并及查询问题。
并查集(UnionFind)具有Union和Find两个功能
- 合并(Union):将两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一个集合中(确定是否具有亲属关系,通过find可以获取其祖先)。
两棵树:
Union(1,5)之后
经典案例:亲戚问题
若某个人家族人员过于庞大,你如何判断其中的两个人是否具有亲戚关系呢?
x和y是亲戚关系,y是z的亲戚,那么x和z也是亲戚。如果x和y是亲戚,那么x的亲戚也是y的亲戚,y的亲戚也是x的亲戚。
这时候我们就可以使用并查集算法,将每一个亲戚关系使用Union关联起来,所有亲戚录入完毕,我们就可以使用并查集中的Find,可以通过Find获取他们的祖先,看看是否是同一个祖先,从而判断他们是不是亲戚。
时间复杂度:elogn(假设有e组集合)
并查集步骤:
- 初始化:把每个点所在的集合初始化为其自身(时间复杂度O(n))
查找:查找两个元素所在的集合,寻找根节点(时间复杂度O(logn))
- 递归查找
- 找到根节点,递归返回时将递归路径所有节点标记为根节点(这样效率会更高,如果每个节点只存储上一级节点效率会更低)
合并:如果两个元素属于不同的集合,将两个元素合并为一个集合(时间复杂度O(1))
- 查找两个元素的根节点
- 先祖不同时,将一个元素的祖先修改为另一个元素的祖先(擒贼先擒王)
UnionFind中数组的意义: 下标代表树的节点,值代表节点的根节点(初始化假设自己是自己的根节点)
我们在使用union(1,2)时数组就会变为
上图的两个树用数组表示就是
使用union(1,5),数组就会变为下图,这就是擒贼先擒王(union(1,9)也是如此):
这使我们调用find(9)就会发生
find(9) -> find(5) -> find(1) 得到1同时将下标9修改为1
在需要使用并查集的场景中,每次使用的框架的都是差不多的,我们只需要对模板进行调整,即可满足大部分需求,下边是一个并查集的算法模板,通过对union简单的优化可以减少祖先设置的次数,从而实现quickUnion。
并查集模板代码:
package com.zhj.leetcode;
public class UnionFind {
public int[] fa;
public int[] rank;
public UnionFind(int length){
init(length);
}
/**
* 初始化,将每个节点初始化为自身
*/
public init(int length){
fa = new int[length];
rank = new int[length];
for (int i = 0; i < length; i++) {
fa[i] = i;
rank[i] = 0;
}
}
/**
* 找到某个元素的根节点
* @param x
* @return
*/
public int find(int x) {
if (x == fa[x]) {
return x;
}
return fa[x] = find(fa[x]);
}
/**
* 合并两个元素为同一个根节点
* @param x
* @param y
*/
public void union(int x, int y) {
int faX = find(x);
int faY = find(y);
if (faX != faY) {
fa[faX] = faY;
}
}
/**
* 合并两个元素为同一个根节点(优化)
* @param x
* @param y
*/
public void quickUnion(int x, int y) {
int faX = find(x);
int faY = find(y);
if (faX != faY) {
if (rank[faX] > rank[faY]) {
fa[faY] = faX;
} else if (rank[faX] < rank[faY]){
fa[faX] = faY;
} else {
fa[faY] = faX;
rank[faX] += 1;
}
}
}
}