并查集的原理及实现

目录

0.引例

1.并查集的原理

2.并查集的存储结构

3.并查集的实现

并查集接口总览

构造函数

查找元素属于哪个集合

判断是否属于同一个集合

合并两个集合

集合的个数

4.并查集完整代码附录

5.并查集在OJ中的应用

省份数量

等式方程的可满足性

0.引例

在我们刚上大学的时候,假如一个宿舍10个人,分别来自广东、湖南、江西;一开始谁也不认识谁,于是广东的只和广东的一起玩,湖南的只和湖南的一起玩,江西的只和江西的一起玩,毕竟是老乡嘛~ ,于是一个宿舍整体被划分成了三个小团体。

为了表示方便,我们给10个人编号,从0开始,于是10个人的编号依次为 0~9:

于是,我们可以很直观的看出谁来自于哪里。 

后来呢,湖南的小伙伴和江西的小伙伴因为共同的口味,都喜欢吃辣的东西,玩在了一块,于是这两个小集体组成了一个大的集体。

我们可以这样划分:

于是,我们可以简单直观的看出爱吃辣的有哪些同学,不爱吃辣的有哪些同学。

当我们划分好了集合之后,就能很方便的进行元素和集合之间、集合和集合之间的操作了,适合进行这些操作的数据结构就是并查集

1.并查集的原理

原理:将n个不同的元素划分成一些不相交的集合,开始时,每个元素自成一个单元素集合,然后按一定的规律将属于同一组元素的集合合并;在此过程中要反复用到查询某一个元素归属于那个集合的运算。(适合描述这种问题的抽象数据结构就叫做 —— 并查集

简单来说,并查集就是能够快速判断一个元素位于哪个集合中,并且能够根据指定条件将不同的集合进行合并的这么一种数据结构。比如上述引例中,根据地域将10个单集合合并成了三个集合,根据是否爱吃辣,把湖南和江西的同学合并成了一个集合。反之,划分好集合之后,我们也可以快速判断一个同学是否爱吃辣?来自哪里?

2.并查集的存储结构

要想实现一个并查集,首先需要确定其底层存储数据的结构,我们使用顺序表,也就是数组来存储。并查集的底层结构和堆类似,用下标表示当前结点和双亲结点之间的关系(重要)。

使用数组存储的理由如下:

  • 使用并查集的时候需要对每个元素从0开始编号,而数组天然就是从0开始编号的结构。
  • 数组支持随机访问,查找效率高。

数组各字段的含义:

  1. 数组的下标对应集合中元素的编号
  2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数(一般选择编号最小的为根)
  3. 数组中如果为非负数,代表该元素双亲在数组中的下标

以引例为例说明一下:

  • 一开始,为每个元素从0开始编号,并且每个元素都是一个集合,并且都代表该集合的根。 

  •  把引例中的集合转化为并查集的形式

  • 用树形结构表示一下各个集合,更加直观

3.并查集的实现

并查集接口总览

首先需要明确的是,使用并查集的时候,我们为每个要存储的数据元素都进行了编号(从0开始),因此,我们在操作并查集的时候,主要通过编号来进行,也就是数组的下标

class UnionFindSet
{
public:// 构造函数UnionFindSet(size_t n):_ufs(n, -1){}// 查找一个元素所在的集合int FindRoot(int x);// 判断两个元素是否在同一个集合中bool InSet(int x1, int x2);// 合并两个元素所在的集合void Union(int x1, int x2);// 获取集合的个数 注意:不是元素个数size_t SetSize();private:vector<int> _ufs;
};

并查集中最重要的接口就是 合并两个集合Union 和 判断两个元素是否在同一个集合中InSet,这两个函数都需要借助 查找根FindRoot来进行,最后还有一个获取集合的个数SetSize。

构造函数

构造函数只需要将并查集中的所有元素值设置为-1即可,表示当前每个元素都是一个集合,集合中有一个元素。

class UnionFindSet
{
public:// 构造函数:指定并查集的大小,将并查集中的元素值全部设置为-1UnionFindSet(size_t n):_ufs(n, -1){}private:vector<int> _set;
};

查找元素属于哪个集合

对于并查集中的每个元素来说,我们需要标识元素属于哪个集合,我们采用集合中编号最小的元素为集合的根,唯一标识一个集合。

  • 编号为0、6、7、8 的元素的根都是0,所以他们属于同一个集合。
  • 编号为1、4、9、2、3、5 的元素的根都是1,所以他们属于同一个集合。

因此,查找一个元素位于哪个集合中只需要查找该元素的根即可,那么如何找到一个元素所在集合的根呢?

  • 不要忘了,并查集中的元素值为负数,负号表示这是一个根,数字表示这个集合中有几个元素;因此,我们只需要沿着数组表示的树形关系一直往上找,直到找到元素的值为负数即可 (即:树中中元素为负数的位置)。

代码如下所示:

int FindRoot(int x)
{int root = x;// 直到_ufs[root] < 0 表示找到根while (_ufs[root] >= 0){root = _ufs[root];}return root;
}

我们可以分析一下: 当多个集合合并时,可能会出现元素到根的路径比较长的情况,路径比较长的话,查找根的效率就会降低,所以我们可以针对这种情况进行代码的优化 —— 路径压缩

  • 路径压缩的优化可以放在FindRoot中进行。
  • 并且只能压缩当前结点到根结点中所涉及的结点。

优化后的代码如下所示:

int FindRoot(int x)
{int root = x;while (_ufs[root] >= 0){root = _ufs[root];}// 路径压缩while (_ufs[x] >= 0)       // 当前元素不是集合的根{int parent = _ufs[x];  // 保存当前结点的双亲结点的下标_ufs[x] = root;        // 更改当前结点的双亲结点x = parent;            // 继续更新保存的双亲结点的双清}return root;
}

路径压缩示意图:以查找编号为3的元素为例,四层路径变成三层了。

判断是否属于同一个集合

因为,我们通过集合的根来判断一个元素属于哪个集合,因此,我们只需要两个元素所在集合的根是否相同,即可判断两个元素是否位于同一个集合中。

bool InSet(int x1, int x2)
{return FindRoot(x1) == FindRoot(x2);
}

合并两个集合

准确来说应该是合并两个元素所在的集合,实现步骤如下:

  1. 判断两个元素所在的集合(通过FindRoot函数即可实现),如果本身就在同一个集合中就没有必要合并了。
  2. 把root2集合中的数据个数加到root1集合中去。
  3. 将root2集合的根修改为root1。

代码如下:

void Union(int x1, int x2)
{// 计算两个元素所在的集合int root1 = FindRoot(x1);int root2 = FindRoot(x2);// 如果本身就在一个集合就没必要合并了if (root1 == root2)return;// 把root2集合中的数据个数加到root1集合中去_ufs[root1] += _ufs[root2];// 将root2集合的根修改为root1_ufs[root2] = root1;
}

我们可以分析一下:我们将一个集合往另一个集合合并,意味着一个集合将变成另一个集合的孩子;两个集合中元素的个数有多有少,是把数据元素多的往数据元素少的合并好,还是把数据元素少的往数据元素多的合并好?

  • 分析可知:肯定是把数据元素少的集合往数据元素多的集合中合并好。比如:A集合往B集合上合并,A集合的根变成B集合的孩子,A集合中剩余的元素变成B集合的后代,如果后代越多,查找根的时间变长,并查集的效率变低。

因此,我们可以对代码进行优化:

void Union(int x1, int x2)
{// 计算两个元素所在的集合int root1 = FindRoot(x1);int root2 = FindRoot(x2);// 如果本身就在一个集合就没必要合并了if (root1 == root2)return;// 优化:控制数据量小的集合往数据量大的集合合并// 保证root2集合中的数据量总是小于root1集合中的数据量if (abs(_ufs[root1]) < abs(_ufs[root2]))swap(root1, root2);// 把root2集合中的数据个数加到root1集合中去_ufs[root1] += _ufs[root2];// 将root2集合的根修改为root1_ufs[root2] = root1;
}

集合的个数

当并查集中的元素的值小于0的时候,说明这是一个集合,遍历查找即可。

size_t SetSize()
{size_t size = 0;// 遍历并查集for (size_t i = 0; i < _ufs.size(); ++i){// _ufs[i] < 0 说明这是一个集合if (_ufs[i] < 0){++size;}}return size;
}

4.并查集完整代码附录

#include <vector>class UnionFindSet
{
public:// 构造函数UnionFindSet(size_t n):_ufs(n, -1){}// 查找指定编号的元素属于哪个集合int FindRoot(int x){int root = x;while (_ufs[root] >= 0){root = _ufs[root];}// 路径压缩while (_ufs[x] >= 0)       // 当前元素不是集合的根{int parent = _ufs[x];  // 保存当前结点的双亲结点的下标_ufs[x] = root;        // 更改当前结点的双亲结点x = parent;            // 继续更新保存的双亲结点的双清}return root;}// 判断两个元素是否属于同一个集合bool InSet(int x1, int x2){return FindRoot(x1) == FindRoot(x2);}// 合并两个元素所在的集合void Union(int x1, int x2){// 计算两个元素所在的集合int root1 = FindRoot(x1);int root2 = FindRoot(x2);// 如果本身就在一个集合就没必要合并了if (root1 == root2)return;// 控制数据量小的集合往数据量大的集合合并// 保证root2集合中的数据量总是小于root1集合中的数据量if (abs(_ufs[root1]) < abs(_ufs[root2]))swap(root1, root2);// 把root2集合中的数据个数加到root1集合中去_ufs[root1] += _ufs[root2];// 将root2集合的根修改为root1_ufs[root2] = root1;}// 获取并查集中集合的个数 注意:不是元素的个数size_t SetSize(){size_t size = 0;// 遍历并查集for (size_t i = 0; i < _ufs.size(); ++i){// _ufs[i] < 0 说明这是一个集合if (_ufs[i] < 0){++size;}}return size;}private:vector<int> _ufs;  // 底层空间复用vector
};

5.并查集在OJ中的应用

省份数量

题目链接:

LCR 116. 省份数量 - 力扣(LeetCode)icon-default.png?t=O83Ahttps://leetcode.cn/problems/bLyHh0/submissions/584995509/

题目描述:

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

解题思路:我们使用并查集,将相连的城市添加到同一个集合中,最后,并查集中集合的个数就是省份的数量。

解题代码:我们可以把前面写的并查集代码拷贝一份过去,直接使用即可。

  • 需要注意的是:该题目中,采用二维数组存储城市之间的关系,此时,也就天然的为每个城市从0开始编号了,也就符合使用我们所写的并查集的前提的。
class UnionFindSet
{
public:// 构造函数UnionFindSet(size_t n):_ufs(n, -1){}// 查找指定编号的元素属于哪个集合int FindRoot(int x){int root = x;while (_ufs[root] >= 0){root = _ufs[root];}// 路径压缩while (_ufs[x] >= 0)       // 当前元素不是集合的根{int parent = _ufs[x];  // 保存当前结点的双亲结点的下标_ufs[x] = root;        // 更改当前结点的双亲结点x = parent;            // 继续更新保存的双亲结点的双清}return root;}// 判断两个元素是否属于同一个集合bool InSet(int x1, int x2){return FindRoot(x1) == FindRoot(x2);}// 合并两个元素所在的集合void Union(int x1, int x2){// 计算两个元素所在的集合int root1 = FindRoot(x1);int root2 = FindRoot(x2);// 如果本身就在一个集合就没必要合并了if (root1 == root2)return;// 控制数据量小的集合往数据量大的集合合并// 保证root2集合中的数据量总是小于root1集合中的数据量if (abs(_ufs[root1]) < abs(_ufs[root2]))swap(root1, root2);// 把root2集合中的数据个数加到root1集合中去_ufs[root1] += _ufs[root2];// 将root2集合的根修改为root1_ufs[root2] = root1;}// 获取并查集中集合的个数 注意:不是元素的个数size_t SetSize(){size_t size = 0;// 遍历并查集for (size_t i = 0; i < _ufs.size(); ++i){// _ufs[i] < 0 说明这是一个集合if (_ufs[i] < 0){++size;}}return size;}private:vector<int> _ufs;  // 底层空间复用vector
};class Solution {
public:int findCircleNum(vector<vector<int>>& isConnected) {// 定义一个并查集UnionFindSet ufs(isConnected.size());// 遍历isConnect矩阵for(int i = 0; i < isConnected.size(); ++i){for(int j = 0; j < isConnected[0].size(); ++j){// 如果相连,就将这两个城市添加到一个集合中if(isConnected[i][j] == 1){ufs.Union(i,j);}}}// 返回并查集中集合的个数即可return ufs.SetSize();}
};

运行结果:

等式方程的可满足性

题目链接:

990. 等式方程的可满足性 - 力扣(LeetCode)icon-default.png?t=O83Ahttps://leetcode.cn/problems/satisfiability-of-equality-equations/submissions/584992038/

题目描述:

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false

示例 1:

输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。

示例 2:

输入:["a==b","b==c","a==c"]
输出:true

解题思路:我们遍历字符串数组,将判断相等的字符添加到同一个集合中;然后再次遍历字符串数组,如果两个字符不相等就不能出现在同一个集合中,如果出现在同一个集合中说明不符合等式方程的可满足性。

  • 需要注意的是,对于该题目,使用我们编写的并查集存储的元素应该是字符,所以需要为字符编号,好在题目中的字符只能是小写字母,所以,我们可以定义一个并查集,开辟26个空间,并全部初始化为-1。
  • 在使用并查集的时候,对小写字符进行编号的映射即可。比如 字符 b -> b - 'a' = 1,映射出的下标即使1了。
  • 但是,我们总不能每次需要并查集的时候都手搓一个并查集吧!因此,我们可以开辟一个vector,借助并查集的思想在vector上进行操作即可。

解题代码

class Solution {
public:bool equationsPossible(vector<string>& equations) {vector<int> ufs(26,-1);// 查找元素位于哪个集合中auto findRoot = [&](int x){int root = x;while(ufs[root] >= 0)root = ufs[root];return root;};// 先处理判断相等的字符串for(auto& str : equations){// 如果相等就添加到同一个集合中// 相当于并查集中的合并两个结合的操作if(str[1] == '='){int root1 = findRoot(str[0]-'a');int root2 = findRoot(str[3]-'a');// 不在一个集合中就合并if(root1 != root2){ufs[root1] += ufs[root2];ufs[root2] = root1;}}}// 再处理判断不想的的字符串for(auto& str : equations){// 此时不能再一个集合中// 相当于并查集中的 判断是否在同一个集合中的操作if(str[1] == '!'){int root1 = findRoot(str[0]-'a');int root2 = findRoot(str[3]-'a');if(root1 == root2)return false;}}return true;}
};

运行结果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/33713.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

机器学习--绪论

开启这一系列文章的初衷&#xff0c;是希望搭建一座通向机器学习世界的桥梁&#xff0c;为有志于探索这一领域的读者提供系统性指引和实践经验分享。随着人工智能和大数据技术的迅猛发展&#xff0c;机器学习已成为推动技术创新和社会变革的重要驱动力。从智能推荐系统到自然语…

Redis使用场景-缓存-缓存雪崩

前言 之前在针对实习面试的博文中讲到Redis在实际开发中的生产问题&#xff0c;其中缓存穿透、击穿、雪崩在面试中问的最频繁&#xff0c;本文加了图解&#xff0c;希望帮助你更直观的了解缓存雪崩&#x1f600; &#xff08;放出之前写的针对实习面试的关于Redis生产问题的博…

RabbitMQ快速入门

文章目录 一. 引入依赖二. 编写生产者代码1. 创建连接2. 创建channel3. 声明一个队列4. 发送消息5. 释放资源6. 运行代码 三. 编写消费者代码1. 创建连接2. 创建channel3. 声明队列4. 消费消息5. 释放资源6. 运行代码 一. 引入依赖 <!-- https://mvnrepository.com/artifact…

如何设计一套对外开放的API体系

生态建设越来越常见&#xff0c;开放自身API给外部伙伴使用&#xff0c;再正常不过&#xff0c;当然不能仅仅暴露个API出来就算完事了&#xff0c;还需要经过详细的设计及验证。 以下是对设计对外开放 API 的每个要点的进一步展开阐述&#xff1a; 明确目标和需求 深入了解业务…

信号和槽思维脑图+相关练习

将登录框中的取消按钮使用信号和槽的机制&#xff0c;关闭界面。 将登录按钮使用信号和槽连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c;密码是否为"123456",如果账号密码匹配成功&#xff0c;当前界面关…

uniapp h5 vue3 m3u8 和 mp4 外链视频播放

m3u8视频播放 使用mui-player 和hls.js。 安装npm install mui-player hls.js我的版本是"hls.js": "^1.5.17"和"mui-player": "^1.8.1"使用 页面标签&#xff1a; 引用&#xff1a; 点击目录播放视频&#xff1a; m3u8视频播放&a…

php项目流水线flow的创建能部署与使用

在云服务器平台上使用 PHP 项目创建、配置、部署和使用流水线&#xff0c;可以通过阿里云 DevOps 服务来自动化 CI/CD&#xff08;持续集成与持续交付&#xff09;流程。以下是详细的步骤和指导&#xff0c;帮助你完成 PHP 项目的流水线设置和部署。 ### 1. 创建流水线 #### …

借助 AI 工具,共享旅游-卡-项目助力年底增收攻略

年底了&#xff0c;大量的商家都在开始筹备搞活动&#xff0c;接下来的双十二、元旦、春节、开门红、寒假&#xff0c;各种活动&#xff0c;目的就是为了拉动新客户。 距离过年还有56 天&#xff0c;如何破局&#xff1f; 1、销售渠道 针对旅游卡项目&#xff0c;主要销售渠道…

软件工程——期末复习(3)

一、题目类(老师重点提到过的题目) 1、高可靠性是否意味着高可用性&#xff1f;试举例证明自己的观点&#xff1f; 答&#xff1a;高可靠性不意味着高可用性 可靠性说明系统已经准备好&#xff0c;马上可以使用&#xff1b;可用性是系统可以无故障的持续运行&#xff0c;是一…

程序员需要具备哪些知识?

程序员需要掌握的知识广泛而深厚&#xff0c;这主要取决于具体从事的领域和技术方向。不过&#xff0c;有些核心知识是共通的&#xff0c;就像建房子的地基一样&#xff0c;下面来讲讲这些关键领域&#xff1a; 1. 编程语言&#xff1a; 无论你是搞前端、后端、移动开发还是嵌…

[Blender]从零开始的blender导入PMX模型教程

一、前言 最近正在接触3D打印&#xff0c;目前我发现&#xff0c;在开源的模型市场上3D的人物模型非常有限并且部分还维持收费。所以就有了一个想法&#xff0c;能不能自己制作3D打印的人物模型。目前虽然开源的3D打印人物模型比较少&#xff0c;但是以PMX开源的人物模型却非常…

C#与PLC通讯时,数据读取和写入浮点数,字节转换问题(ModbusTCP)

在与PLC进行通讯时&#xff0c;会发现一个问题&#xff0c;浮点数1.2接收过来后&#xff0c;居然变成了两个16位的整数。 经过一系列的分析&#xff0c;这是因为在PLC存储浮点数时32位&#xff0c;我们接收过来的数据会变成两个16位的高低字节&#xff0c;而且我们进行下发数据…

替代FTP最佳跨网文件传输解决方案——FileLink

在传统的企业文件传输中&#xff0c;FTP&#xff08;文件传输协议&#xff09;曾因其便捷性和高效性被广泛应用。然而&#xff0c;其固有的安全漏洞、对大文件传输支持的局限性、易受网络攻击等问题&#xff0c;已逐渐暴露出FTP在现代企业环境下的不足。针对这一问题&#xff0…

纯粹直播 1.7.7 |手机版和TV版,聚合六大直播平台,原画播放

纯粹直播是一款开源的应用程序&#xff0c;支持兴趣化主题的游戏直播、户外直播和才艺直播节目。目前可以观看斗鱼、B站、虎牙和抖音等六大直播平台的内容。该应用适配了安卓手机和电视盒子平台使用&#xff0c;并且软件无广告&#xff0c;提供原画质播放体验。 大小&#xff…

汉诺塔(递归)

递归、搜索与回溯算法 文章目录 递归、搜索与回溯算法前言一、递归的思想二、汉诺塔三、为什么可以使用递归思想&#xff1f;四、代码实现 Leetcode汉诺塔 前言 这是记录我学习算法的一个专题&#xff0c;如果你正在备战这类比赛&#xff0c;我想这对你一定有帮助。 一、递归…

【JUC-锁升级】简要版本

锁升级过程 一、偏向锁二、轻量级锁三、重量级锁四、整体流程 为什么不全部使用Synchronized、Lock等重量级锁呢&#xff1f; 重量级锁底层是基于操作系统的互斥锁实现的&#xff0c;涉及到用户态与内核态之间的切换。 一、偏向锁 如果只有一个线程A频繁的访问某一个共享资源…

C++小碗菜之二:软件单元测试

“没有测试的代码重构不能称之为重构&#xff0c;它仅仅是垃圾代码的到处移动” ——Corey Haines 目录 前言 什么是单元测试&#xff1f; 单元测试的组成 单元测试的命名 单元测试的独立性 Google Test 单元测试的环境配置与使用 1. Ubuntu下安装 Google Test 2. 编写…

家庭财务管理系统的设计与实现ssm小程序+论文源码调试讲解

2系统关键技术 2.1 微信小程序 微信小程序&#xff0c;简称小程序&#xff0c;英文名Mini Program&#xff0c;是一种全新的连接用户与服务的方式&#xff0c;可以快速访问、快速传播&#xff0c;并具有良好的使用体验。 小程序的主要开发语言是JavaScript&#xff0c;它与普…

linux运维命令

防火墙相关命令 防火墙规则查看 firewall-cmd --list-all 禁ping firewall-cmd --permanent --add-rich-rulerule protocol valueicmp drop firewall-cmd --reload 执行完以上命令后&#xff0c;通过firewall-cmd --list-all查看规则生效情况 firewall-cmd --list-all 其…

高通---Camera调试流程及常见问题分析

文章目录 一、概述二、Camera配置的整体流程三、Camera的代码架构图四、Camera数据流的传递五、camera debug FAQ 一、概述 在调试camera过程中&#xff0c;经常会遇到各种状况&#xff0c;本篇文章对camera调试的流程进行梳理。对常见问题的提供一些解题思路。 二、Camera配…