西丽做网站/网站制作400哪家好
数据结构:并查集
- 并查集
- 原理
- 实现
- 框架
- 初始化
- 合并
- 查询
- 获取成员
- 路径压缩
- 其它
- 总代码
并查集
在生活中,经常会出现分组问题。比如一个班级分为多个小组,打篮球分为两方等等。在同一个组中的所有成员,就构成一个集合。对这种一个群体分为多个集合的数据结构,称为并查集
。
其提供两个最核心的功能:
合并
:将两个集合合并成一个集合查询
:查找两个元素是否属于一个集合
因此称为并查集。
实现一个并查集并不难,但是如果要实现一个高效的并查集,就需要一定的设计了。本博客讲解以C++
实现的并查集,并且尽可能在时间与空间的利用上更加高效。
原理
谈到集合,在数据结构中如何维护一个集合?比如一个数组,一个set
,一棵树等等。既然要探求一个最高效的存储方式,那么就要讨论如何最大化利用资源了。
如果使用一个数组来存储一个集合,那么每个集合都要开辟一个数组,在合并集合时,还需要发生数组的合并,此时又会有空间的开辟和销毁。
如果使用链式树存储集合,此时合并就会很方便:
红色与蓝色是两个不同的集合,合并集合时,只需要修改一个指针的指向即可。
但是链式结构也有问题,链式结构的数据是分散的,计算机每次加载节点都需要寻址,效率很低。有没有方法既可以保持树结构,又可以集中的存储所有数据?
如果你学习过堆
,那么答案就呼之欲出了,其实就是使用一个数组形式的树。
如图,每个节点存储自己的父节点的下标,根节点存储自己的下标。
其可以转化为如下三个集合:
这是一种常见的并查集形式,但是还可以再优化。这种形式下根节点存储自己的下标,是不是可以把这块空间腾出来,存储该集合的元素个数?
如图,根节点存储的值变为负数,绝对值表示该集合的总元素个数。为什么根节点要变为负数?之前已经规定了:数组的元素存储自己父节点的下标,如果根节点的值为一个正整数,此时如何判断这是一个根节点还是普通节点,存储的值是集合总元素还是父节点下标?
因为数组下标没有负数,所以此时就可以通过正负数判断该节点是根节点还是普通节点:
负数
:根节点,存储该集合元素总个数正数
:普通节点,存储父节点的下标
这是一个非常高效的存储结构,使用一个数组就表示了一个并查集,内含多个树结构。而多棵树在一起就构成了一个森林,其实并查集的本质就是一个森林。
但是至此还有一个问题,这个并查集只能表示整数集合,不能表示其它的string
等类型,所以还需要一个map
维持映射关系,将其他元素映射为数组下标。
实现
框架
为了提高可扩展性,把并查集定义为一个类模板,模板参数为并查集存储元素的类型。
template <typename T>
class UnionFindSet
{
private:vector<int> _ufs;map<T, int> _mp;
};
成员变量:
_ufs
:并查集的本体,用于维护集合的关系,也就是刚刚设计的那个数组_mp
:一个映射关系,将存储的元素T
映射到具体的数组下标int
初始化
初始化时并查集接收一个数组,里面是独立的元素,它们不构成任何集合关系。
随后要构建这些元素与下标的映射关系,即初始化_mp
。另
最后,对于_ufs
本体,全部初始化为-1
。
因为一开始所有元素自成一个集合,都是集合的根节点,而根节点存储的是集合元素的个数的负数。每个集合只有一个元素,所以节点值初始化为-1
。
构造函数:
UnionFindSet(vector<T>& source): _ufs(source.size(), -1)
{for (int i = 0; i < source.size(); i++)_mp[source[i]] = i;
}
参数接受一个数组source
,内部包含多个T
类型元素,在初始化列表种将_ufs
的大小扩大到与source
一致,所有元素初始化为-1
。
在函数体内部,完成对_mp
的初始化,遍历source
,存储(source[i], i)
的映射关系。
合并
合并两个集合,就是将其中一个元素的根节点的父节点指针,指向另一个节点的根节点,如图:
上图展示了蓝色集合与绿色集合的合并操作,分为以下两步:
- 将蓝色集合根节点的值加上绿色集合根节点的值:
-4
变-7
- 将绿色集合的根节点的值变为蓝色集合根节点的下标:
-3
变0
既然要操作集合的根节点,自然就要先找到集合的根节点,写一个函数用于获取集合根节点:
int findRoot(T x)
{if (_mp.count(x) == 0)throw runtime_error("value does not exist"); // 值不存在int root = _mp[x];while (_ufs[root] >= 0){root = _ufs[root];}return root;
}
首先通过_mp.count(x)
判断该元素是否在并查集种,如果不在就抛出一个异常,表示值不存在。
随后通过一个循环,每次root = _ufs[root]
,其中_ufs[root]
是父节点的下标,这样就可以让root
往父节点走,直到走到根节点,此时_ufs[root]
是一个负数,最后跳出循环返回根节点。
找到根节点后,就可以完成集合的合并操作了:
void unionSet(T x1, T x2)
{int root1 = findRoot(x1);int root2 = findRoot(x2);if (root1 == root2)return;_ufs[root1] += _ufs[root2];_ufs[root2] = root1;
}
首先通过findRoot
找到两个集合的根节点,如果根节点相同,说明两个元素本来就处于一个集合种,直接返回。
随后_ufs[root1] += _ufs[root2];
完成了元素的加和,此时root1
是新根,_ufs[root1]
存储的是两个集合的元素总和的负数。
最后_ufs[root2] = root1;
,修改toor2
父节点,完成集合的合并。
这里还有一个优化,两个集合有两种合并方式:
如图,可以将绿色集合合并到蓝色集合下,也可以将蓝色集合合并到绿色集合下。这两种方式都是合理的,但是哪一种更好?
在集合种查找元素时,最多搜索树的高度次,树高度越低,那么搜索效率就越高。所以常把集合元素多的作为根。上图中因为蓝色集合元素个数多,所以把绿色集合合并到蓝色集合更优,也就是左边的方式。这个优化称为按秩合并
。
代码优化:
void unionSet(T x1, T x2)
{int root1 = findRoot(x1);int root2 = findRoot(x2);if (root1 == root2)return;// 按秩合并if (_ufs[root1] < _ufs[root2]){_ufs[root1] += _ufs[root2];_ufs[root2] = root1;}else{_ufs[root2] += _ufs[root1];_ufs[root1] = root2;}
}
由于根节点存储的就是集合的元素个数,所以可以直接拿_ufs[root]
来比较两个集合的大小。如果_ufs[root1] < _ufs[root2]
,因为根节点存储的是负数,所以_ufs[root1]
的绝对值更大,要把root2
合并到root1
。
查询
并查集的第二个核心操作是判断两个元素是否在同一个集合。这其实非常简单,只需要判断两个元素的根节点是否相同即可!
bool inSet(T x1, T x2)
{return findRoot(x1) == findRoot(x2);
}
获取成员
该接口的作用是,输入一个元素,取同一集合中的其它所有元素。
刚刚讲解过,判度两个元素是否在同一个集合,只需要看根节点是否相同。所以此处只需要:
- 先获取输入的根节点
root
- 遍历整个并查集,判度根节点是否与
root
相同
vector<T> getMembers(T x)
{vector<T> members;int root = findRoot(x);for (const auto& pair : _mp){if (findRoot(pair.first) == root) members.push_back(pair.first);}return members;
}
以上代码返回一个vector<T>
,里面是与x
为同一集合的所有元素。
首先root = findRoot(x)
,获取x
的根节点。随后通过for
循环遍历_mp
,findRoot(pair.first)
获取元素根节点,再与root
判等,如果相等说明在同一集合,此时尾插到members
数组中。
路径压缩
当并查集使用久了,就会出现树高度太高的问题,但是并查集内部的树是多叉树,如下图两个集合:
这两个集合其实是同一个集合,但是很明显左边的树高度低,查询效率会高很多。所以并查集中常会做一个优化,将树高度尽可能降低,这个优化称为路径压缩
。
压缩路径被实现在查找操作findRoot
中,因为每次查找的时候,都会从树底网上遍历到根节点,这是完成路径压缩的最好时机。
路径压缩的算法核心是:
每次向上查找父节点时,把自己提高到与父节点的同一层
如图:
当前从节点4
开始向上查找,首先找到父节点1
,随后将4
提升到与1
的同一层。也就是中间的情况。
此时问题变成了:从1
开始查找根节点。找到父节点7
,随后将1
提升到与7
的同一层,此时就变成了最后一种情况。
最后找到根节点为0
,由于0
已经是根节点了,不能把7
提升到根节点。
实现:
int findRoot(T x)
{if (_mp.count(x) == 0)throw runtime_error("value does not exist"); // 值不存在int root = _mp[x];while (_ufs[root] >= 0 && _ufs[_ufs[root]] >= 0){_ufs[root] = _ufs[_ufs[root]]; // 路径压缩}if (_ufs[root] >= 0)root = _ufs[root];return root;
}
由于路径压缩要考虑爷爷节点是否存在,所以while
内部有两个条件:_ufs[root] >= 0
表示父节点存在,_ufs[_ufs[root]] >= 0
表示爷爷节点存在。
只要父节点和爷爷节点都存在,那么就可以进行路径压缩,_ufs[root] = _ufs[_ufs[root]]
,其中_ufs[root]
是当前节点的值存储的是父节点的下标,_ufs[_ufs[root]]
是爷爷节点的下标。这个赋值将爷爷节点的下标赋值给自己,此时就把爷爷节点变成了父节点,完成了向上提升。
最后while
循环离开的时候,有可能是因为爷爷节点不存在,此时root
是根节点的某一个孩子,所以还要root = _ufs[root]
往上走一层。
其它
还有一些其它的小接口,都很简单
- 当前并查集内部有多少个集合
size_t count()
{size_t size = 0;for (auto& num : _ufs){if (num < 0)size++;}return size;
}
- 输入一个集合,获取该集合的元素个数
size_t size(T x)
{return abs(_ufs[findRoot(x)]);
}
想要知道集合元素个数,只需要找到根节点,然后返回绝对值即可。
总代码
UnionFindSet.hpp
#pragma once
#include <iostream>
#include <vector>
#include <map>
#include <stdexcept>using namespace std;template <typename T>
class UnionFindSet
{
public:UnionFindSet(vector<T>& source): _ufs(source.size(), -1){for (int i = 0; i < source.size(); i++)_mp[source[i]] = i;}int findRoot(T x){if (_mp.count(x) == 0)throw runtime_error("value does not exist"); // 值不存在int root = _mp[x];while (_ufs[root] >= 0 && _ufs[_ufs[root]] >= 0){_ufs[root] = _ufs[_ufs[root]]; // 压缩路径root = _ufs[root];}if (_ufs[root] >= 0)root = _ufs[root];return root;}void unionSet(T x1, T x2){int root1 = findRoot(x1);int root2 = findRoot(x2);if (root1 == root2)return;// 按秩合并if (_ufs[root1] < _ufs[root2]){_ufs[root1] += _ufs[root2];_ufs[root2] = root1;}else{_ufs[root2] += _ufs[root1];_ufs[root1] = root2;}}bool inSet(T x1, T x2){return findRoot(x1) == findRoot(x2);}size_t count(){size_t size = 0;for (auto& num : _ufs){if (num < 0)size++;}return size;}size_t size(T x){return abs(_ufs[findRoot(x)]);}vector<T> getMembers(T x) {vector<T> members;int root = findRoot(x);for (const auto& pair : _mp){if (findRoot(pair.first) == root) members.push_back(pair.first);}return members;}private:vector<int> _ufs;map<T, int> _mp;
};
test.cpp
,测试代码
#include <iostream>
#include <string>
#include <vector>
#include "unionFindSet.hpp"using namespace std;int main()
{vector<string> stu = { "张三", "李四", "王五", "赵六", "翠花", "小龙", "小淘", "小明" };UnionFindSet<string> ufs(stu);cout << ufs.count() << endl;cout << ufs.inSet("张三", "翠花") << endl;ufs.unionSet("张三", "赵六");ufs.unionSet("王五", "小淘");ufs.unionSet("翠花", "小明");ufs.unionSet("翠花", "张三");cout << ufs.inSet("张三", "翠花") << endl;cout << ufs.count() << endl;cout << ufs.size("张三") << endl;auto members = ufs.getMembers("张三");for (auto& mem : members)cout << mem << " ";cout << endl;return 0;
}
相关文章:

数据结构:并查集
数据结构:并查集 并查集原理实现框架初始化合并查询获取成员路径压缩其它 总代码 并查集 在生活中,经常会出现分组问题。比如一个班级分为多个小组,打篮球分为两方等等。在同一个组中的所有成员,就构成一个集合。对这种一个群体分…...

微信小程序实战教程:轻松实现列表批量选择功能
在许多场景下,用户需要对列表中的多项内容进行操作,如批量删除、批量下载等。为了满足这一需求,我们需要在微信小程序中实现列表批量选择功能。具体要求如下: 用户可以逐个选择列表项,也可通过全选按钮快速选择所有列表…...

企业微信:开启客户联系和配置
前言 客户联系是企业微信的一项非常实用且自定义化配置丰富的功能,使企业内的授权员工可以添加外部客户(企业微信联系人和微信联系人)进行工作沟通,并且还可以建立客户群,甚至发表内容到客户朋友圈! 由于功…...

Python发送邮件教程:如何实现自动化发信?
Python发送邮件有哪些方法?如何利用python发送邮件? 无论是工作汇报、客户通知还是个人提醒,邮件都能快速传递信息。Python发送邮件的自动化功能就显得尤为重要。AokSend将详细介绍如何使用Python发送邮件,实现自动化发信&#x…...

一周热门|苏姿丰:芯片行业不能只盯着 GPU;Gartner:GenAI 即将越过期望膨胀期
大模型周报将从【企业动态】【技术前瞻】【政策法规】【专家观点】四部分,带你快速跟进大模型行业热门动态。 01 企业动态 Open AI 计划从非营利组织向营利组织转型 日前,路透社报道称,OpenAI 正在制定一项计划,将其核心业务重…...

Failed to load WebView provider: No WebView installed
1、问题 使用webview加载网页,在应用运行时,报了如下错误:android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed2、分析 通过查看项目的修改记录,确实安装了We…...

java日志框架之Log4j
文章目录 一、Log4j简介二、Log4j组件介绍1、Loggers (日志记录器)2、Appenders(输出控制器)3、Layout(日志格式化器) 三、Log4j快速入门四、Log4j自定义配置文件输出日志1、输出到控制台2、输出到文件3、输出到数据库 五、Log4j自…...

C++ bitset(位图)的模拟实现
文章目录 一、bitset接口总览二、bitset模拟实现1. 构造函数2. set、reset、flip、test3. size、count4. any、none、all5. 打印函数 三、完整代码 一、bitset接口总览 成员函数功能set设置指定位或所有位为1(即设置为“已设置”状态)reset清空指定位或…...

Llama 3.2:利用开放、可定制的模型实现边缘人工智能和视觉革命
在我们发布 Llama 3.1 模型群后的两个月内,包括 405B - 第一个开放的前沿级人工智能模型在内,它们所产生的影响令我们兴奋不已。 虽然这些模型非常强大,但我们也认识到,使用它们进行构建需要大量的计算资源和专业知识。 我们也听到…...

解决R语言bug ‘sh‘ is not recognized as an internal or external command
安装源码包‘httr2’ trying URL ‘https://cran.rstudio.com/src/contrib/httr2_1.0.5.tar.gz’ Content type ‘application/x-gzip’ length 230632 bytes (225 KB) downloaded 225 KB installing source package ‘httr2’ … ** package ‘httr2’ successfully unpacked…...

记一次Mac 匪夷所思终端常用网络命令恢复记录
一天莫名奇妙发现ping dig 等基础命令都无法正常使用。还好能浏览器能正常访问,,,, 赶紧拿baidu试试^-^ ; <<>> DiG 9.10.6 <<>> baidu.com ;; global options: cmd ;; connection timed out; no serve…...

2024最新!!Java后端面试题(4)看这一篇就够了!!!!
七、异常 throw 和 throws 的区别? throw用来显式地抛出一个异常,而throws则用于在方法声明中指明该方法可能抛出的异常。简单来说,throw是抛出异常的实际动作,throws是告知调用者这个方法可能会抛出哪些异常的声明。 final、f…...

springboot整合sentinel和对feign熔断降级
一、准备 docker安装好sentinel-dashboard(sentinel控制台),参考docker安装好各个组件的命令启动sentinel-dashboard,我的虚拟机ip为192.168.200.131,sentinel-dashboard的端口为8858 二、整合sentinel的主要工作 在…...

遗传算法与深度学习实战——使用进化策略实现EvoLisa
遗传算法与深度学习实战——使用进化策略实现EvoLisa 0. 前言1. 使用进化策略实现 EvoLisa2. 运行结果相关链接 0. 前言 我们已经学习了进化策略 (Evolutionary Strategies, ES) 的基本原理,并且尝试使用 ES 解决了函数逼近问题。函数逼近是一个很好的基准问题&…...

HttpServletRequest简介
HttpServletRequest是什么? HttpServletRequest是一个接口,其父接口是ServletRequest;HttpServletRequest是Tomcat将请求报文转换封装而来的对象,在Tomcat调用service方法时传入;HttpServletRequest代表客户端发来的请…...

c++开发之编译curl(安卓版本)
为了在 Android 上编译支持 OpenSSL 的 libcurl,你需要手动编译 libcurl 和 OpenSSL,并确保它们能够在 Android 的交叉编译环境中正常工作。以下是详细的步骤说明。 1. 安装必要工具 在编译之前,确保你已经安装了以下工具: And…...

QT+ESP8266+STM32项目构建三部曲三--QT从环境配置到源程序的解析
一、阿里云环境配置 大家在编写QT连接阿里云的程序之前,先按照下面这篇文章让消息可以在阿里云上顺利流转 QTESP8266STM32项目构建三部曲二--阿里云云端处理之云产品流转-CSDN博客文章浏览阅读485次,点赞7次,收藏4次。创建两个设备ÿ…...

Web APIs 5:Window对象(BOM)+本地存储
Web APIs 5(BOM:Window对象本地存储) 1.BOM(浏览器对象模型)(后面几个对象都为BOM对象) BOM对象包含:navigator、location、document(DOM对象)、history、screenBOM是一个全局对象,即JS中的顶…...

神经网络(四):UNet图像分割网络
文章目录 一、简介二、网络结构2.1编码器部分2.2解码器部分2.3完整代码 三、实战案例 论文链接:点击跳转 一、简介 UNet网络是一种用于图像分割的卷积神经网络,其特点是采用了U型网络结构,因此称为UNet。该网络具有编码器和解码器结构&#…...

Java 编码系列:注解处理器详解与面试题解析
引言 在上一篇文章中,我们详细探讨了 Java 注解的基本概念、自定义注解、元注解等技术。本文将继续深入探讨 Java 注解处理器(Annotation Processor),介绍如何编写注解处理器,并结合大厂的最佳实践和面试题详细解析其…...

C语言 | Leetcode C语言题解之第441题排列硬币
题目: 题解: class Solution { public:int arrangeCoins(int n) {return (int) ((sqrt((long long) 8 * n 1) - 1) / 2);} };...

Linux noVNC远程桌面(xfce)部署
一、安装 VNC 服务器和桌面环境 Notebook实验 常用vnc服务 VNC (Virtual Network Computing) 是一种远程桌面协议,可以让你通过网络访问服务器的图形界面。 TurboVNC:专为图形密集型应用设计,尤其适合 3D 可视化和高分辨率图像的远程传输…...

【网络安全】身份认证
1. 身份认证 1.1 定义 身份认证(Authentication)是确认用户身份的过程,确保只有授权的用户才能访问系统或资源。它通常涉及验证用户提供的凭证,如密码、生物特征或其他识别标志。 1.2 重要性 身份认证是信息安全的第一道防线&…...

LeetCode - #124 二叉树中的最大路径和(Top 100)
文章目录 前言1. 描述2. 示例3. 答案关于我们前言 本题为 LeetCode 前 100 高频题 我们社区陆续会将顾毅(Netflix 增长黑客,《iOS 面试之道》作者,ACE 职业健身教练。)的 Swift 算法题题解整理为文字版以方便大家学习与阅读。 LeetCode 算法到目前我们已经更新到 123 期…...

Java:插入排序
目录 排序的概念 插入排序 直接插入排序 哈希排序 排序的概念 排序:所谓的排序,就是使一串记录,按照某个或某些关键字的大小,递增或递减的排列起来的操作。 稳定性:假定在待排序的记录序列中,存在多个…...

How FAR ARE WE FROM AGI?(ICLR AGI Workshop 2024)概览
关注B站可以观看更多实战教学视频:hallo128的个人空间 How FAR ARE WE FROM AGI?官网 How FAR ARE WE FROM AGI?(ICLR AGI Workshop 2024) 该研讨会将于2024年5月11日在奥地利维也纳以混合模式举行,作为 ICLR 2024年会议的一部…...

leetcode刷题day33|动态规划Part02(62.不同路径、63. 不同路径 II、 343.整数拆分、96.不同的二叉搜索树)
62.不同路径 机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。 动规五部曲 1、确定dp数组(dp table)以及下标的含义 dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路…...

基于Python大数据的B站热门视频的数据分析及可视化系统
作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏:Java精选实战项目…...

matlab-批处理图像质量变化并形成折线图 (PSNR)
%修改路径就能用,图片分辨率要一致 %clc;clear all;close all;tic;%清理内存 file_pathE:\test\resources\image\;% 批量图像所在的文件夹下 file_save_pathE:\test\resources\SaveImage\;% 要存储的地址 img_path_listdir(strcat(file_path,*.jpg));% 获取批量bm…...

[Doc][Ros2]ros2中Qos(Quality of Service,服务质量)介绍
在 ROS 2 中,QoS(Quality of Service,服务质量)是用于控制节点之间消息传递的可靠性、历史存储和数据持久性等方面的机制。通过 QoS 设置,用户可以更细粒度地控制消息传递的行为,确保在不同网络环境或应用场景中满足特定的通信需求。 几个常用的包: QoSProfile: 含义…...