算法学习笔记(4): 并查集及其优化

算法,学习,笔记,及其,优化 · 浏览次数 : 46

小编点评

**并查集**是一种可以动态维护多个集合的数据结构,支持集合之间的合并和查询。 **主要功能:** * Find:查询元素所属集合 * Merge:将两个元素所属集合合并 **优化方案:** * 路径压缩:将树形结构转换成graph TD,以提高查询效率 * 启发式合并:使用cnt数组优化元素合并 * 按秩合并:根据元素在树中的层级排序,合并具有更优的效率

正文

并查集

并查集,Disjoint-Set,或者通俗一点,叫做MergeFind-Set,是一种可以动态维护若干个不重叠的集合,并支持集合之间的合并与查询的数据结构。

集体来说,并查集支持下列两个操作:

  • Find,查询元素所属集合

  • Merge,将两个元素所属集合合并

一般来说,为了具体实现,我们将每一个集合选择一个固定的元素,作为整个集合的代表

所以,假设我们的元素是一堆整数,则,可以有

// N需要根据实际情况调整,grp是group的缩写
int grp[N];

初始化则设每一元素所在的组是其本身

for (int i = 0; i <= n; ++i) grp[i] = i;

做完了准备工作,我们需要思考如何合并?

考虑我们可以使用树形结构来储存,则,每一个树根都应该满足grp[root] = root

于是我们只需要修改树根就行了,即grp[root(x)] = root(y)

那么,问题来了,如何寻找根?

回到上述定义中的树形结构及其性质,则很容易可以推出一个递归算法:

int find(int x) {
    if (grp[x] != x) return find(grp[x]);
    return x;
}

于是,合并的算法也就可以写出来了

int merge(int x, int y) {
    grp[find(x)] = find(y);
}

优化

优化方案有3:路径压缩启发式合并按秩合并

路径压缩

其实,我们把整个grp的关系链画出来

graph TD; 1-->2; 1-->3; 2-->4; 4-->5;

实际上,我们可以不关注树的形状,意味着上图中的树实际上等价于

graph TD; 1-->2; 1-->3; 1-->4; 1-->5;

这样,我们就可以在find中将这个元素直接连接到其父节点上

则有

int find(int x) {
    if (grp[x] != x) return grp[x] = find(grp[x]);
    return x;
}

其实很多时候,只用路径压缩就已经够了

启发式合并

看上去很高级的名字,其实原理很简单

我们新建一个cnt数组,由于记录每一个元素所在集合中有多少个元素,在合并时,将元素多的作为根,则可以相对优化。

但是,毕竟是启发式合并,元素多的并不一定层数少,这是一个概率问题……

但好处是,启发式合并可以和路径压缩一起使用,这比只使用路径压缩快了一些,并且代码复杂度比较简单。所以我就不提供参考代码了

按秩合并

这里的就是指的深度,那么,为了优化,很明显,我们需要将层数少的合并到层数多的树中,且层数少的合并到层数多的并不会影响层数多的层数。

但是,如果两者层数相等呢?

举个例子,我们要合并14

graph TD; 1-->2; 1-->3; 4-->5;

那么由于两棵树层数相等,所以无论哪一个作为主树都可(假设我们以1为主树)

则合并之后应该时

graph TD; 1-->2; 1-->3; 1-->4; 4-->5;

可以发现,层数变多了……所以说,需要+1

分析完毕,代码该如何写?

首先时初始化,由于刚开始每一个元素都是一颗树,所以为1

int rank[N]; // 储存每一个元素的秩
for (int i = 0; i <= n; ++i) {
    rank[i] = 1;
    grp[i] = i;
}

寻找的代码不变

合并代码如下:

void merge(int x, int y) {
    int gx = find(x), gy = find(y);
    if (rank[gx] <= rank[gy])
        grp[gx] = gy;
    else
        grp[gy] = gx;
    if (rank[gx] == rank[gy] && gx != gy) // 防止合并相同元素
        ++rank[y];
}

按秩合并也可以和路径压缩一起用。

有证明其复杂度可以接近于常数


扩展

并查集也可以应用在最小生成树算法中,其中一个比较经典的算法:kruskal 算法就用到了并查集的辅助,而 kruskal 重构树更是好玩:算法学习笔记(30):Kruskal 重构树 - jeefy - 博客园

这方面可以参考其他资料,这里不做过多的展开。

并查集多用于树上问题,在环的处理上非常高效。


练习


所以,并查集你应该掌握了,下课!

与算法学习笔记(4): 并查集及其优化相似的内容:

算法学习笔记(4): 并查集及其优化

# 并查集 并查集,Disjoint-Set,或者通俗一点,叫做MergeFind-Set,是一种可以动态维护若干个不重叠的集合,并支持集合之间的合并与查询的数据结构。 集体来说,并查集支持下列两个操作: - Find,查询元素所属集合 - Merge,将两个元素所属集合合并 一般来说,为了具体实现

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

VisionPro学习笔记(4)——PatInspect

如果需要了解其他图像处理的文章,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice VisionPro有很多的示例和算子,这里再展示一个最新出的算子Pat Inspect Tool。

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解

C++算法之旅、08 基础篇 | 质数、约数

算法学习笔记,记录容易忘记的知识点和难题。试除法、分解质因数、筛质数、约数个数、约数之和、最大公约数

算法学习笔记(1): 欧几里得算法及其扩展

扩展欧几里得算法详解 在了解扩欧之前我们应该先了解欧几里得算法 欧几里得算法 这是一个递归求最大公约数(greatest common divisor)的方法 $$ gcd(a, b) = gcd(b, a % b) $$ 可以通过一个类似的简单公式推导而来 好像叫做辗转相减法来着? $$ gcd(

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知