【狂热算法篇】并查集:探秘图论中的 “连通神器”,解锁动态连通性的神秘力量
嘿,朋友们!喜欢这个并查集的讲解吗 记得点个关注哦,让我们一起探索算法的奥秘,别忘了一键三连,你的支持是我最大的动力!
欢迎拜访:羑悻的小杀马特.-CSDN博客本篇主题:
深度剖析并查集算法及一系列优化制作日期:2025.01.13
隶属专栏:美妙的算法世界
下面我们就介绍一下并查集是啥吧:
目录
一·概念:
二·数据结构表示:
三·基本操作及实现:
3.1初始化:
3.2查找操作(root):
3.3 合并操作(merge):
朴素版:
四.优化策略:
4.1路径压缩:
4.2按秩合并:
4.3按大小合并(启发合并):
五·优化前后对比:
5.1 优化前:
5.2优化后:
六·例题测试:
七.应用场景:
7.1网络连通性问题:
7.2社交网络中的朋友圈问题:
7.3:图论中的最小生成树算法(如 Kruskal 算法):
八·个人小结:
一·概念:
并查集是一种树型的数据结构,用于处理不相交集合的合并及查询问题。它主要支持两种操作:“并”(Union)操作,即将两个不相交的集合合并为一个集合;“查”(Find)操作,即查找元素所在的集合(大概它的外形也可以理解为多插树的形式)。
并查集在解决元素分组和动态连通性问题上展现出强大的能力,能够高效地处理元素之间的关系,判断元素是否属于同一集合,以及将不同集合进行合并操作。
这里我们就记住:同根就连通,合并总发生在根上。
二·数据结构表示:
数组存储:
最常见的实现方式是使用一个数组 pre[]
来存储每个元素的父节点信息。初始时,每个元素自成一个集合,所以 pre[i] = i
,表示元素 i
是它自己集合的代表元素即根节点.
当然了,还会有其他表示方法:
也可以使用链表、树等数据结构表示并查集,但数组是最常用的,因为其实现简单,操作相对便捷,并且在经过优化后可以达到理想的性能。
三·基本操作及实现:
3.1初始化:
void init() {for (int i = 1; i <= n; i++) {pre[i] = i;//无连接把自身初始化成前驱节点}return;
}
这里就不用多解释了把,一开始还没给关系,它们每个节点自己就是一个集合,自己是自己的根节点。
3.2查找操作(root):
功能:查找元素所在集合的代表元素(根节点)。
通过不断查找元素的父节点,直到找到父节点为自身的元素,该元素即为集合的代表元素。
这里我们可以用循环来实现也可以用递归;但是如果它深度太高还是循环比较友好,但是一般数据又不会太大。
比如我们举个例子:
例如,对于集合
{0, 1, 2}
,其中pre[0] = 0
,pre[1] = 0
,pre[2] = 1
,查找元素2
的根节点时,先找到pre[2] = 1
,继续查找pre[1] = 0
,最终找到根节点为0。
下面就用普通的递归实现我们的查找操作:
int root(int x) {return pre[x]==x?x:root(pre[x]);//递归找到根节点
}
3.3 合并操作(merge):
功能:将两个元素所在的集合合并为一个集合。
首先找到两个元素的根节点,如果根节点不同,则将其中一个根节点作为另一个根节点的子节点,完成集合的合并。
注意:合并操作不是就是找根节点嘛,这里我们可以规定方向但是也可以不规定:前提是,我们保证所合并的集合的每个节点的通过父亲节点找到的根节点一定是同一个即可。(但是对于朴素法,也就是我们上面所写的,还没有做优化,可以认为后者的根节点点是前者的根节点的根节点)
下面就是简单版本的合并:
void merge(int i, int j) {int x = root(i), y = root(j);pre[x]=y;return;
}
这样就是实现好了。其实上面实现的就是我们的朴素版本。
朴素版:
const int N = 5005;/// <summary>
/// 多插树
/// </summary>
int pre[N];//类似前驱节点,pre[i]是i指向的前一个节点j:i->j
int rk[N];//每个节点的秩:高度
int sz[N];//以i号节点作为根节点的集合的节点个数
int n, m, p;void init() {for (int i = 1; i <= n; i++) {pre[i] = i;//无连接把自身初始化成前驱节点}return;
}
//找根节点:
int root(int x) {return pre[x]==x?x:root(pre[x]);//递归找到根节点
}
//合并总发生在根节点:
void merge(int i, int j) {int x = root(i), y = root(j);pre[x]=y;return;
}
但是这样肯定是时间复杂度肯定比较高的,可以认为为O(N);那么下面我们来介绍一下它的优化策略。
四.优化策略:
下面我们分三种情况来把朴素版给优化一下:
4.1路径压缩:
介绍:在查找操作时,将查找路径上的元素的父节点都直接指向根节点,以减少后续查找的时间复杂度。
那么我们只需要,在查找元素的根节点时,将路径上的元素的父节点直接更新为根节点。
也就是我们最后的目的就是让每个点的父亲节点都是他们的根节点即可。
当然了它的实现可以是循环解决也可以是递归,但是对于数据不是特别大,我们就选递归,因为它比较好写:
4.1.1递归版本:
//找根节点:
int root(int x) {return pre[x]=pre[x] == x ? x : root(pre[x]);//递归找到根节点,归的时候完成对当前节点的最终指向(根节点)}
4.1.2迭代版本:
int root(int x){int r=x;while(pre[r]>=0){r=pre[r];}//拿到根:while(pre[r]>=0){int res=pre[x];pre[x]=r;x=res;} return r;}
这个优化只需要我们更改找根操作即可,其他和朴素法一样。
但是这样就一定好嘛,我们之前说的是它是一个多插树,这样就破坏了,树的结构,当然也是可以用的(只限于单个数据结构,也就是只有它自己);但是如果还要和其他数据结构的功能配合使用,那么就麻烦了;因此我们就要建议使用后面的优化方法了。
时间复杂度就降到O(1)了。
总代码:
const int N = 5005;/// <summary>
/// 多插树
/// </summary>
int pre[N];//类似前驱节点,pre[i]是i指向的前一个节点j:i->j
int rk[N];//每个节点的秩:高度
int sz[N];//以i号节点作为根节点的集合的节点个数
int n, m, p;路径压缩:破坏树结构void init() {for (int i = 1; i <= n; i++) {pre[i] = i;//无连接把自身初始化成前驱节点}return;
}
//找根节点:
int root(int x) {return pre[x]=pre[x] == x ? x : root(pre[x]);//递归找到根节点,归的时候完成对当前节点的最终指向(根节点)}
//合并总发生在根节点:
void merge(int i, int j) {int x = root(i), y = root(j);pre[x] = y;return;
}
4.2按秩合并:
目的:避免合并操作使树变得过于不平衡,影响查找性能。
为每个节点维护一个秩(rank),可以是树的高度或集合的大小,即叶子节点秩为0,合并时将秩小的集合合并到秩大的集合,秩相等时任意合并并更新秩。
说白了也就是让我们的这颗树变的矮一点,胖一点而不是高高瘦瘦的;那这样为什么能起到优化作用呢?
我们的秩就是树高,如果我们让秩小的和并时候指向高的,也就是让高树的根节点成为矮树根节点的父亲节点;这样我们在查询高树的N多个底层节点的时候就可以少走一次了 ;矮树多走一步,但是它相对走的少啊。
那么这就得出结论了:
让秩小的根节点指向秩大的根节点;如果相同呢?那么随便指向,但是此时我们就要更新最终根节点的秩,也就是加一了(这里其实就不完全考虑我们上面朴素法定义的关系指向了,只需要保证同集合共根即可)。
时间复杂度就近似O(logN)了。
定义rk数组存放每个节点的秩:
void merge(int i, int j) {int x = root(i), y = root(j);if (rk[x] < rk[y]) swap(x, y);//保证x的秩大。秩大的指向秩小的:(根节点)pre[y] = x;if (rk[x] == rk[y])rk[x]++;//如果相等则根节点可以互相指向,但是被指向的节点秩要更新return;
}
然而这里我们只需要更改合并操作;对于查找的操作(我们也可以修改成循环形式):
总代码:
const int N = 5005;/// <summary>
/// 多插树
/// </summary>
int pre[N];//类似前驱节点,pre[i]是i指向的前一个节点j:i->j
int rk[N];//每个节点的秩:高度
int sz[N];//以i号节点作为根节点的集合的节点个数
int n, m, p;/// /按秩合并:根据高度关系让root函数查找的时候找的更快(每个节点都少走一个,提高效率)
void init() {
for (int i = 1; i <= n; i++) {pre[i] = i;//无连接把自身初始化成前驱节点
}
return;
}
//找根节点:
int root(int x) {//return pre[x] == x ? x : root(pre[x]);//递归找到根节点while (pre[x] != x) x = pre[x];return x;
}
//合并总发生在根节点:
void merge(int i, int j) {int x = root(i), y = root(j);if (rk[x] < rk[y]) swap(x, y);//保证x的秩大。秩大的指向秩小的:(根节点)pre[y] = x;if (rk[x] == rk[y])rk[x]++;//如果相等则根节点可以互相指向,但是被指向的节点秩要更新return;
}
4.3按大小合并(启发合并):
这里其实和按秩排序优化效果上一样;只不过不是根据秩划分;而是根据集合的大小来划分罢了。
就是我们合并的时候,肯定会有多个集合;那么我们要想集合里的元素越多,那么它向上找根次数肯定越多,那么如果一个大集合和一个小集合合并是不是让大集合的根指向小集合的根就可以少走很多次了(类似按秩合并思想,只是写法不同罢了)
时间复杂度就近似O(logN)了。
因此得到总结:
小集合根节点指向大集合根节点;如果相同,两个根节点可以随机指向;其次注意合并后要更新集合大小。
因此我们搞一个数组为sz[]记录每个集合大小:
也就是:int sz[N];以i号节点作为根节点的集合的节点个数 。
但是这里还有个细节(很容易忽略):我们这个操作不仅要改变指向,集合大小,还要注意初始化,每个节点一开始的集合大小就是1;不像按秩合并一样(因为叶子节点本身秩就是0):
void init() {for (int i = 1; i <= n; i++) {pre[i] = i;//无连接把自身初始化成前驱节点sz[i] = 1;//注意数组含义(集合节点数)/细节/}return;
}
合并:
void merge(int i, int j) {int x = root(i), y = root(j);if (sz[x] < sz[y]) swap(x, y);//保证x是大集合:pre[y] = x;sz[x] += sz[y];//有y集合的纳入,x变大,的sz也增大return;
}
总代码:
const int N = 5005;/// <summary>
/// 多插树
/// </summary>
int pre[N];//类似前驱节点,pre[i]是i指向的前一个节点j:i->j
int rk[N];//每个节点的秩:高度
int sz[N];//以i号节点作为根节点的集合的节点个数
int n, m, p;/// /按大小合并(启发合并):
void init() {for (int i = 1; i <= n; i++) {pre[i] = i;//无连接把自身初始化成前驱节点sz[i] = 1;//注意数组含义(集合节点数)/细节/}return;
}
//找根节点:
int root(int x) {// return pre[x] == x ? x : root(pre[x]);//递归找到根节点while (pre[x] != x) x = pre[x];return x;
}
//合并总发生在根节点:
void merge(int i, int j) {int x = root(i), y = root(j);if (sz[x] < sz[y]) swap(x, y);//保证x是大集合:pre[y] = x;sz[x] += sz[y];//有y集合的纳入,x变大,的sz也增大return;
}
五·优化前后对比:
5.1 优化前:
单次查找或合并操作的最坏情况时间复杂度为 o(N),因为树可能退化成链,查找元素时可能需要遍历整个链。
5.2优化后:
经过路径压缩和按秩合并优化,单次查找或合并操作的平均时间复杂度接近o(
(N)) ,其中
(N) 是阿克曼函数的反函数,其增长速度非常慢,在实际应用中可近似认为是常数时间复杂度,因此优化后的并查集在效率上有显著提升。
六·例题测试:
下面我们就以一道洛谷的并查集模版题测试一下吧:
输入:
6 5 3 1 2 1 5 3 4 5 2 1 3 1 4 2 3 5 6
输出:
Yes Yes No
数据范围(还是比较小的):
链接: 亲戚 - 洛谷
首先我们每个写法无论优化还是不优化都是可以通过的:
这里数据范围比较小,所以优化肯定是成立的但是我们一般看不太出来:
这里按大小合并确实有点离谱了,但是数据还是比较少的;因此理论应该是和按秩合并一样的。
main函数实现:
int main() {ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);//取消同步cin >> n >> m >> p;init();//初始化,自己指向自己while (m--) {int i, j;cin >> i >> j;merge(i, j);//合并:完成节点的指向i->j。}while (p--) {int a, b;cin >> a >> b;if (root(a) == root(b)) cout << "Yes" << endl;//根节点相同属于同一个节点else cout << "No" << endl;}}
七.应用场景:
7.1网络连通性问题:
在计算机网络中,判断两台计算机是否处于同一子网,将计算机视为元素,当新的连接建立时,可以使用并查集合并集合。
例如,在网络拓扑中,每台计算机开始是独立的集合,当连接两台计算机时,通过并查集的合并操作将它们所在集合合并,表示它们处于同一连通区域。
7.2社交网络中的朋友圈问题:
判断两个人是否属于同一朋友圈,添加好友时可以合并两个朋友圈集合。
比如在社交软件中,每个人开始是一个独立的朋友圈,当添加好友时,使用并查集将两人所在的朋友圈集合合并。
7.3:图论中的最小生成树算法(如 Kruskal 算法):
用于判断边的两个端点是否在同一连通分量,若不在,则合并它们所在的连通分量。
在 Kruskal 算法中,对边进行排序,依次取出边,若边的两端不在同一集合,使用并查集的合并操作,最终形成最小生成树。
因此:
并查集作为一种高效的数据结构,通过简单的数组存储和优化的查找、合并操作,在元素分组、动态连通性判断等方面具有广泛的应用。它在解决网络、社交和图算法等领域的问题时,能够提供简洁有效的解决方案,优化后的并查集在性能上表现出色,是处理集合操作和图论问题的重要工具之一。
八·个人小结:
个人认为,我们在进行实现时候,要注意两点:同根即连通,合并总发生在根上(指向无所谓,只要保证同一个集合元素一定都同根即可);其次就是使用:判断是否有关系,组别划分等;根据题目分析即可。
相关文章:

【狂热算法篇】并查集:探秘图论中的 “连通神器”,解锁动态连通性的神秘力量
嘿,朋友们!喜欢这个并查集的讲解吗 记得点个关注哦,让我们一起探索算法的奥秘,别忘了一键三连,你的支持是我最大的动力! 欢迎拜访:羑悻的小杀马特.-CSDN博客 本篇主题:深度剖析并查…...

SpringBoot中实现动态数据源切换
SpringBoot中实现动态数据源切换 文章目录 SpringBoot中实现动态数据源切换SpringBoot中实现动态数据源切换基础知识1. 什么是数据源?2. 动态数据源切换的概念3. Spring Boot 中的默认数据源配置4. 动态数据源的挑战5. Spring 中的数据源切换方式 设计思路1. 明确应…...

数据结构及排序算法
数据结构 线性结构 ◆线性结构:每个元素最多只有一个出度和一个入度,表现为一条线状。线性表按存储方式分为顺序表和链表。 存储结构: ◆顺序存储:用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素物理上也相邻。 ◆链式存储:存储各数据元素的结点…...

Python基础-元组tuple的学习
在 Python 中,元组(tuple)是一种不可变的序列类型,允许存储不同类型的元素。元组非常类似于列表(list),但与列表不同的是,元组一旦创建,就不能修改其内容。 1 元组的创建…...

【手写公式识别】MEMix: Improving HMER with Diverse Formula Structure Augmentation 论文阅读
发表于:ICME 2024 原文链接:https://ieeexplore.ieee.org/document/10687521 源码:无 Abstract 手写数学表达式识别(HMER)旨在将数学表达式(MEs)的图像转换为相应的LaTeX序列。然而࿰…...
使用deepseek写一个飞机大战游戏
说明: 安装pygame:在运行代码之前,需要先安装 pygame 库。可以通过以下命令安装: pip install pygame图像文件:需要将玩家、敌人和子弹的图像文件(player.png, enemy.png, bullet.png)放在与脚本…...
用Kibana实现Elasticsearch索引的增删改查:实战指南
在大数据时代,Elasticsearch(简称 ES)和 Kibana 作为强大的数据搜索与可视化工具,受到了众多开发者的青睐。Kibana 提供了一个直观的界面,可以方便地对 Elasticsearch 中的数据进行操作。本文将详细介绍如何使用 Kiban…...
C# 封送和远程编程介绍
.NET学习资料 .NET学习资料 .NET学习资料 在 C# 编程领域中,封送(Marshaling)和远程编程(Remote Programming)是两个极为重要的概念,它们为开发者提供了与不同环境、不同进程或不同机器上的代码进行交互的…...
MybatisPlus较全常用复杂查询引例(limit、orderby、groupby、having、like...)
MyBatis-Plus 是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。以下是 MyBatis-Plus 中常用复杂查询(如 LIMIT、ORDER BY、GROUP BY、HAVING、LIKE 等)的引例: 1. 环境准备…...

02.07 TCP服务器与客户端的搭建
一.思维导图 二.使用动态协议包实现服务器与客户端 1. 协议包的结构定义 首先,是协议包的结构定义。在两段代码中,pack_t结构体都被用来表示协议包: typedef struct Pack {int size; // 记录整个协议包的实际大小enum Type type; …...
Jenkins数据备份到windows FTP服务器
文章目录 背景1. 安装配置 FileZilla Server(Windows)1.1 下载并安装 FileZilla Server1.2 配置 FTP 用户和共享目录 2. 安装并配置 FTP 客户端(CentOS)2.1 在 CentOS 安装 lftp 3. 编写 Jenkins 备份脚本3.1 赋予执行权限3.2 测试…...

【R语言】卡方检验
一、定义 卡方检验是用来检验样本观测次数与理论或总体次数之间差异性的推断性统计方法,其原理是比较观测值与理论值之间的差异。两者之间的差异越小,检验的结果越不容易达到显著水平;反之,检验结果越可能达到显著水平。 二、用…...
ASP.NET Core托管服务
目录 托管服务的异常问题 托管服务中使用DI 托管服务案例:数据的定时导出 场景,代码运行在后台。比如服务器启动的时候在后台预先加载数据到缓存,每天凌晨3点把数据导出到备份数据库,每隔5秒钟在两张表之间同步一次数据。托管服…...

HarmonyOS 5.0应用开发——全局自定义弹出框openCustomDialog
【高心星出品】 文章目录 全局自定义弹出框openCustomDialog案例开发步骤完整代码 全局自定义弹出框openCustomDialog CustomDialog是自定义弹出框,可用于广告、中奖、警告、软件更新等与用户交互响应操作。开发者可以通过CustomDialogController类显示自定义弹出框…...
如何在C++ QT 程序中集成cef3开源浏览器组件去显示网页?
文章目录 1. **准备工作**1.1 下载CEF31.2 配置Qt项目2. **集成CEF3到Qt窗口**2.1 创建Qt窗口容器2.2 初始化CEF33. **处理CEF3消息循环**4. **处理多进程架构**5. **完整代码示例**`main.cpp`6. **常见问题**6.1 黑屏问题6.2 窗口嵌入失败6.3 多进程调试7.**Github源码参考**8…...
深入讲解MyBatis
1. MyBatis 的背景和优势 背景:在 Java 开发中,传统的 JDBC 操作数据库代码繁琐,需要手动管理数据库连接、编写 SQL 语句、处理结果集等,开发效率低且容易出错。MyBatis 应运而生,它通过将 SQL 语句与 Java 代码分离&a…...

使用matlab 对传递函数分析bode图和阶跃函数
如果已知一个系统的传递函数,想看一下bode图,可以通过simulink 建模,但是simulink运行起来相对比较慢,我一般都是直接通过matlab 的m语言写脚本实现。可以快速的获得结果 如 我们有一个一阶低通传递函数 syswn/(swn) 在matlab中…...
2025牛客寒假算法基础集训营5(补题)
C 小L的位运算 显然,如果两次反置的价格小于等于交换的价格,那么直接全部反置就好了。 反之,由于交换一定低于两次反置,我们尽可能用交换来消去不正确的位置。不正确的位置类型只有00,01,10,11&…...
FaceFusion如何设置公开链接和端口
有时候我们想在局域网内的其他设备上使用 FaceFusion,这时候需要设置公开链接和端口。 当你运行 FaceFusion 的时候,会发现有这样的一段提示: To create a public link, set shareTrue in launch().但是这个提示是错的,如果你查…...

神经网络常见激活函数 6-RReLU函数
文章目录 RReLU函数导函数函数和导函数图像优缺点pytorch中的RReLU函数tensorflow 中的RReLU函数 RReLU 随机修正线性单元:Randomized Leaky ReLU 函数导函数 RReLU函数 R R e L U { x x ≥ 0 a x x < 0 \rm RReLU \left\{ \begin{array}{} x \quad x \ge 0…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
C++中string流知识详解和示例
一、概览与类体系 C 提供三种基于内存字符串的流,定义在 <sstream> 中: std::istringstream:输入流,从已有字符串中读取并解析。std::ostringstream:输出流,向内部缓冲区写入内容,最终取…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

初学 pytest 记录
安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

MySQL 知识小结(一)
一、my.cnf配置详解 我们知道安装MySQL有两种方式来安装咱们的MySQL数据库,分别是二进制安装编译数据库或者使用三方yum来进行安装,第三方yum的安装相对于二进制压缩包的安装更快捷,但是文件存放起来数据比较冗余,用二进制能够更好管理咱们M…...

深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化
缓存架构 代码结构 代码详情 功能点: 多级缓存,先查本地缓存,再查Redis,最后才查数据库热点数据重建逻辑使用分布式锁,二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
计算机系统结构复习-名词解释2
1.定向:在某条指令产生计算结果之前,其他指令并不真正立即需要该计算结果,如果能够将该计算结果从其产生的地方直接送到其他指令中需要它的地方,那么就可以避免停顿。 2.多级存储层次:由若干个采用不同实现技术的存储…...