前言
这篇整理主要是给自己做个记录,这里做参考的文章要好的多。(有涉及到树和图的知识)
参考自知乎《算法学习笔记(1) : 并查集》
一、什么是并查集,用在哪?
关于定义,引用百度百科的原文:
并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。
这句话中提取出如下信息。
本质 | 树型的数据结构 |
---|---|
处理对象 | 一些不相交集合 |
操作 | 合并、查询 |
对这个算法的介绍,网上通常引用“找亲戚的例子”:有n个由若干个不同的家族组成的人,我们只知道所有人中父(母)与子(女)的关系,判断其中的A和B是否是有血缘关系。
我们可以顺着A的关系一步一步找,再顺着B的关系一步一步找。
1. 比如A的爸爸是C,C的爸爸是D,D没有爸爸,那么我们可以认为D是A的爷爷,并且D是这个家族中最大的一辈人,那我们就可以简单的称这个家族为D家族。
2. 同理,我们假设在所有人找到B的爸爸是X,并且X没有爸爸,我们简单的称B和X所在的家族是X家族。
3. 出于某种原因,我们并没有找到X和D的爸爸是谁,现实生活中我们无法判断X和D有没有血缘关系,但是在这里,X,D作为最顶层的一辈人,我们把他俩看作是根节点,根节点和根节点没有血缘关系。所以作为X子级的B和D子级的子级的A没有血缘关系。
4. 同理,若A和B最终的根节点指向同一个,那就说明他们必然是有血缘关系的。
二、并查集的原理
并查集的原理就是如“找亲戚”的例子一样,我们把其中的每个人都看作是一个独立的集合,当知道这个集合的根节点(一个人作为一个集合的时候,他自己就是这个根节点)和另一个集合的节点有关系时,我们就把这两个集合合并一个大集合,进行若干次合并之后,剩下的集合之间没有关系时,不再合并。
查询操作同理,分别查询两个子节点的根节点,如果两个子节点的根节点是同一个,说明他们存在某种联系。
三、并查集的几种简单写法
1.原理法
int fa[MAXN];
void init(int n)
{
for (int i = 1; i <= n; ++i)
fa[i] = i;//初始化节点,每个节点的父节点是他自己
}
int merge()//合并
{
//根据某种规则,建立节点之间的联系。
//如点1指向点2,则fa[1]=2;说明点1的父节点是点2.
}
int find(int x)
{
if(fa[x] == x)//在合并操作之后,如果这个节点的父节点是他自己,说明这个点就是根节点。
return x;
else //否则,继续查找x父节点的父节点是不是他本身。
return find(fa[x]);
}
2.路径压缩法
本质我们是想找到两个节点之间的关系,在查找他们的根节点的时候需要顺着父节点一个一个遍历,如果能知道某个节点的根节点是哪个,并且直接让这个节点指向这个根节点,当所有的子节点只有根节点这一个父节点的时候,再次查询的时间复杂度就降为O(1).(可以看一下开头参考文章的图)
int find(int x)
{
return x == fa[x] ? x : (fa[x] = find(fa[x]));
//如果x的父节点是他本身,那么这个x的节点就是根节点
//否则让x的父节点等于最后查到找的根节点
}
3.按秩合并法
原理法中,我们选择的合并规则是,谁是父节点谁就最大,把最大的作为根节点。
想想一下,如果一颗树的中间某层上的节点只有一个,那么我是不是可以把这个这个树从那一层“掰开”,让中间那个节点作为父节点,这样就可以降低整棵树的深度。
例如在下图我有一颗深度是6的树。
把它折半,⑤看成是根节点。
这样深度为6的树就变成深度为4的树了。
虽然改变了树的结构,但是这几个元素任然在一个集合中(一颗树上)。在上面的例子中,并不能影响判断两个人是不是有血缘关系,降低树的深度反而加快了算法的速度。
同理,当我们发现某个集合的根节点和另一个集合有关系时,需要适当选取根节点进行合并。
void init(int n)
{
for (int i = 1; i <= n; ++i)
{
fa[i] = i;
rank[i] = 1;
}
}
void merge(int i, int j)
{
int x = find(i), y = find(j); //先找到两个根节点
if (rank[x] <= rank[y])
fa[x] = y;
else
fa[y] = x;
if (rank[x] == rank[y] && x != y)
rank[y]++; //如果深度相同且根节点不同,则新的根节点的深度+1
}