重谈并查集

重谈并查集

当初学并查集的时候记得是森哥讲的@zcs0724。但是没太学明白(是因为蒟蒻太菜,跟森哥没半毛钱关系),于是今天来重新捋一下并查集,顺便补个讲解。就叫重谈并查集吧。(没有爆粗)(滕总笑)


并查集的概念

并查集(Disjoint-Set)是一种可以维护若干个不重叠的集合,并支持集合合并与查找的数据结构。

(来自李煜东《算法竞赛进阶指南》)

简单来讲,就是一个支持两个集合合并,支持查询一个元素在哪个集合里的数据结构。也就是有合并和查找两种操作。


并查集的实现思路

刚刚提到的并查集的基本概念是建立在“集合”这个定义上的。那么,我们肯定要选择一种方法来把集合表示出来。可以看出的是,如果对集合从1开始进行编号,那么最终的效率应该是很低的,所以我们选择用一个代表元素来表示这个元素所在的整个集合。

那么,如果选择维护一个数组\(f[i]\)表示\(i\)元素所在集合的编号(也就是代表元素),在每次合并的时候需要大量修改\(f[i]\),效率非常低。于是,我们继续思考,使用常见的数据结构:树来存储集合,令代表元素为树根。那么,一个并查集就是一个森林,一棵树就是一个集合。我们只需要维护\(fa[i]\)表示这个节点的父亲,特别的,令树根的父亲为他自己。那么,在我们查询的时候,就可以一直递归查到根节点,即为答案。合并的时候,直接令一棵树的树根为另一棵树的树根的子节点即可。

但是递归就很慢。

怎么办呢?


路径压缩

刚刚提到过,我们的并查集已经优化到通过维护树来维护集合。但是在查询的时候,这样的最坏复杂度是\(O(N)\)级别的。所以我们要考虑如何对其进行优化。

思考优化其实就是在思考如何把最坏状况变成最好状况。很容易看出,这棵树的最好状况是只有两层,也就是所有节点要么是根节点,要么是它的儿子。

灵光一闪:那么,如果我们在每次查询的时候,都把路径上经过的所有节点都直接指向根节点呢?是不是在之后的查询中就快很多了?复杂度是均摊\(O(\log N)\)的。


启发式合并

关于启发式合并,是多种数据结构合并时的一种思想,具体的蒟蒻有专门随笔讲解:

浅谈启发式合并


并查集的代码实现

在这里给出带注释的路径压缩并查集的代码实现:

int find(int x)
{
    if(x==fa[x])
        return x;//如果查到根节点
    fa[x]=find(fa[x]);//过程中路径压缩
}
int find(int x)
{
    return x==fa[x]?x:fa[x]=find(fa[x]);//以上代码的简化
}
void merge(int x,int y)//合并x,y元素所在的集合
{
    int fx=find(x);
    int fy=find(y);//先查
    fa[fx]=fy;//直接合并
}
posted @ 2020-09-22 08:23  Seaway-Fu  阅读(216)  评论(0编辑  收藏  举报