主要用于解决一些元素分组问题,管理一系列不相交的集合,支持两种操作
合并(Union):把两个不相交的集合合并为一个集合
查询(Find):查询两个元素是否在同一个集合中
重要思想:用集合中的一个元素代表集合,就好比一个帮派用一个帮主来代表
并查集的基本思想非常简单,代码较为容易实现
//初始化,这里用father来保存每个元素的父节点,一开始,使每个元素的父节点都是自己本身
int[] father;
void init(int n){
father = new int[n];
for(int i=0;i<n;i++){
father[i] = i;
}
}
//查询,要判断两个节点是否属于同一个集合,只需要看他们的根节点是否相同即可,所以这里查询的时候只需要不断的向上一层一层的访文父节点,直至根节点(根节点的标志就是其父节点就是自己本身)
int find(int x){
if(father[x] == x){
return x;
}else{
return find(father[x]);
}
}
//合并, 这里直接把i的根节点修改为j(反之将j的根节点修改为i也可以)
void merge(int i, int j){
father[find[i]] = j;
}
并查集的基本功能以及得以实现,但是效率在某些情况下较为低下,比如现在有{1,2} 和 {3},现在希望merge(2,3),于是由2找到1,father[1] = 3,变成了{3,1,2},这时还有一个集合{4},希望能够merge(2,4),于是由2找到1找到3,然后father[3] = 4,变成了{4,3,1,2}
显然,随着元素的增多,这条链会变得越来越长,由一个任意节点找到对应的根节点会变得越来越慢,此时,就可以压缩路径了,也就是把每一个节点的父节点都直接设置为根节点,每一次的查询都会变得十分简单:
//压缩路径之后的查找
int find(int x){
if(x==father[x]{
return x;
}else{
//将路径上的每一个节点的父节点直接设置为根节点
father[x] =find(father[x]);
return father[x];
}
}
此时,并查集的时间复杂度已经可以令人接受了,但是考虑以下的情况:
当希望合并{4,3,1,2} 和 {5}的时候,选择father[4] = 5更好,还是选择father[5] = 4更好呢?答案应该是显而易见的,选择father[5] = 4可以更大限度的减少树的深度,使用一个rank[]数组来记录每一个根节点所对应的树的深度,一开始,将每一个元素的rank都设置为1,在合并的时候,先比较两个根节点的rank,把rank较小的节点往rank较大的节点上合并
以下就是按秩合并和路径压缩后的代码
//初始化
int[] father;
int[] rank;
void init(int n){
father = new int[n];
rank = new int[n];
for(int i=0;i<n;i++){
father[i] = i;
rank[i] = 1;
}
}
//查找
int find(int x){
if(x==father[x]){
return x;
}else{
//将路径上的每一个节点的父节点直接设置为根节点
father[x] =find(father[x]);
return father[x];
}
}
//合并
void merge(int i, int j){
int x = find(i);
int y = find(j);
if(x!=y){
//按秩合并
if(rank[x]<rank[y]){
//这里总是让x的rank保持较大,y的rank保持较小
int temp = x;
x = y;
y = temp;
}
father[y] = x;
if(rank[x]==rank[y]){
//这里不用判断合并后到底是rankx[x]++还是rank[y]++就是因为在上面我们总是让y合并在x上
rank[x]++;
}
}
}
这里需要注意,当路径压缩和按秩合并一起使用时,我们对于rank增加的更新总是能够及时的做出反应,但是rank的减少却不能很好的及时更新,但是实际上rank不能及时更新减少,并不会对结果造成太大影响,而且对rank保持持续的实时更新,代码会更加的复杂,所以这里不去更新rank的减少