并查集Disjoint Set

并查集的概念

并查集是一种简单的集合表示,它支持以下三种操作

1. make_set(x),建立一个新集合,唯一的元素是x
2. find_set(x),返回一个指针,该指针指向包含x的唯一集合的代表,也就是x的根节点
3. union_set(x,y),将包含x和y的两个集合合并成一个新的集合

并查集的存储结构

通常用树的双亲表示作为并查集的存储结构,每个子集合以一棵树表示。某节点的根节点代表了该结点所属的集合。所有表示子集合的树,构成表示全集合的森林。

经过union_set(0,6),union_set(6,7),union_set(0,8)后形成S1

经过union_set(1,4),union_set(4,9)后形成S2

经过union_set(2,5),union_set(2,3)后形成S3

并查集的基本实现

存储结构定义

使用树的双亲表示法(链式存储)。当然也可以顺序存储实现,顺序存储实现我放在了最下面的例题中。

using ElemType = int;
struct DisjointSetNode
{ElemType data;//数据struct DisjointSetNode* parent;//指向父节点
};

make_set(x)

新建一个集合,集合中唯一的元素是x,此时x是根节点,x的父节点指针指向自己。

void make_set(DisjointSetNode* node)
{if(node != nullptr){node->parent = node;}else{throw std::invalid_argument("make_set error: node must not be nullptr!");}
}

find_set(x)

返回一个指针,该指针指向包含x的唯一集合的代表,也就是x的根节点。根节点的判断条件:node->parent == node。只需递归地寻找父结点,直到node->parent == node。

时间复杂度与树的高度相关,最坏时间复杂度为O(n)

DisjointSetNode* find_set(DisjointSetNode* node)
{if(node == nullptr){throw std::invalid_argument("make_set error: node must not be nullptr!");}if(node->parent != node){node = find_set(node->parent);}return node;
}

union_set(x,y)

将包含x和y的两个集合合并成一个新的集合,我的代码实现是将y并入x。单只是合并,时间复杂度为O(1)。算上两次find_set的时间复杂度为O(n)。

void link_set(DisjointSetNode* rootX, DisjointSetNode* rootY)
{//集合X的根节点为rootX,集合Y的根节点为rootY,将集合X与集合Y合并if(rootX == nullptr || rootY == nullptr){throw std::invalid_argument("make_set error: node must not be nullptr!");}rootY->parent = rootX;
}void union_set(DisjointSetNode* nodeX, DisjointSetNode* nodeY)
{DisjointSetNode* rootX = find_set(nodeX);DisjointSetNode* rootY = find_set(nodeY);if(rootX == rootY){//如果nodeX和nodeY是同一个集合的元素,则不能合并return;}link_set(rootX, rootY);
}

按秩合并和路径压缩

上面提到,并和查的时间开销主要在“查”上,而查的时间复杂度与树的高度相关,是O(h)。

那么只要降低树的高度,就能大幅降低时间开销。

降低树高度有两种方法,一是按秩合并,二是路径压缩。

按秩合并

每个结点x维持一个整数值属性rank,它代表了x的高度(x的叶结点到x的最长路径长度)。在按秩合并的union操作中,我们让具有较小秩的根指向具有较大秩的根


路径压缩

在`find_set`操作中,使查找路径中的每个结点直接指向树根

最终代码

链式存储实现

//
// Created by user on 2024/3/16.
//
#include <stdexcept>
//并查集 disjoint set
//并查集包括三个操作,1. make_set(x),建立一个新集合,唯一的元素是x
//2. find_set(x),返回一个指针,该指针指向包含x的唯一集合的代表,也就是x的根节点
//3. union_set(x,y),将包含x和y的两个集合合并成一个新的集合/*
* 这里采用了启发式策略改进运行时间,使用了两种启发式策略:
*   - 按秩合并:每个结点x维持一个整数值属性rank,它代表了x的高度(从x到某一后代叶结点的最长简单路径上的结点数目)的一个上界。在按秩合并的union操作中,
*   我们让具有较小秩的根指向具有较大秩的根
*   - 路径压缩:在`find_set`操作中,使查找路径中的每个结点直接指向树根** 如果单独采用按秩合并或者路径压缩,它们每一个都能改善不相交集合森林上操作的运行时间;而一起使用这两种启发式策略时,这种改善更大。
* 当同时使用按秩合并和路径压缩时,最坏情况下的运行时间为O(m*alpha*n)),这里alpha(n)是一个增长非常慢的函数。在任何一个可以想得到的不相交集合数据结构的应用中,
* alpha(n)<=4。其中n为结点个数,m为操作次数(运用了摊还分析)*/
using ElemType = int;
struct DisjointSetNode
{ElemType data;//数据struct DisjointSetNode* parent;//指向父节点int rank;//rank代表结点的高度,即从该节点到叶子节点的最长路径的长度
};void make_set(DisjointSetNode* node)
{if(node != nullptr){node->parent = node;node->rank = 0;}else{throw std::invalid_argument("make_set error: node must not be nullptr!");}
}/*
* 该操作简单沿着指向父节点的指针找到树的根。树的根的特征是:它的父节点就是它本身。
* 若结点不在不相交集合森林中(当结点的父节点指针为空时),则抛出异常。
*
* find_set过程是一个 two_pass method,当它递归时,第一趟沿着查找路径向上直到找到树根;
* 当递归回溯时,第二趟沿着搜索树向下更新每个节点,使其父节点直接指向树根
* find_set 包含“压缩路径”优化
*/
//find_set
DisjointSetNode* find_set(DisjointSetNode* node)
{if(node == nullptr){throw std::invalid_argument("make_set error: node must not be nullptr!");}if(node->parent != node){node->parent = find_set(node->parent);}return node->parent;
}//每个结点x维持一个整数值属性rank,它代表了x的高度
//(从x到某一后代叶结点的最长简单路径上的结点数目)的一个上界。
// 在链接时我们让具有较小秩的根指向具有较大秩的根,这样可以降低树的深度,从而减少find_set的时间消耗
void link_set(DisjointSetNode* rootX, DisjointSetNode* rootY)
{//集合X的根节点为rootX,集合Y的根节点为rootY,将集合X与集合Y合并if(rootX == nullptr || rootY == nullptr){throw std::invalid_argument("make_set error: node must not be nullptr!");}if(rootX->rank > rootY->rank){//假如集合X的深度大于集合Y的深度,则要将集合Y的根节点指向集合X的根节点,以尽可能地降低深度rootY->parent = rootX;}else{rootX->parent = rootY;if(rootX->rank == rootY->rank){//如果深度一样,此时是将X并入Y,则需要让rootY->rank加一++rootY->rank;}}
}void union_set(DisjointSetNode* nodeX, DisjointSetNode* nodeY)
{DisjointSetNode* rootX = find_set(nodeX);DisjointSetNode* rootY = find_set(nodeY);if(rootX == rootY){//如果nodeX和nodeY是同一个集合的元素,则不能合并return;}link_set(rootX, rootY);
}

例题

547.省份数量 - 力扣(LeetCode)

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

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

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

返回矩阵中 省份 的数量。

这里用了顺序存储实现并查集,并使用了路径压缩按秩合并的优化方法。时间复杂度为O(α(n) * n^2),其中α(n)是一个增长极其缓慢的函数,通常α(n)≤4

class Solution {
public:vector<int> parent;//parent[x]代表x的父节点vector<int> rank;//rank[x]代表x的秩,也就是x到叶结点的最长路径长度//并查集的顺序存储方式。//如何判断结点x是否是树的根节点? parent[x] == x//如何找到根结点?   if(parent[x] != x) {递归调用}int findCircleNum(vector<vector<int>>& isConnected) {int province_num = 0;int size = isConnected.size();initDisjointSet(parent, rank, size);for(int i = 0;i < size;++i){for(int j = i+1;j < size;++j){if(isConnected[i][j] == 1){//假如i与j相连,则合并i,j所在的集合union_set(parent, i, j);}}}for(int i = 0;i < size;++i){if(parent[i] == i){++province_num;}}return province_num;}void initDisjointSet(vector<int>& parent, vector<int>& rank, int size){parent.resize(size);rank.resize(size, 0);//rank的初始值为0for(int i = 0;i < size;++i){//初始化并查集,此时每个元素本身自成一个集合parent[i] = i;}}
/*
* find_set过程是一个 two_pass method,当它递归时,第一趟沿着查找路径向上直到找到树根;
* 当递归回溯时,第二趟沿着搜索树向下更新每个节点,使其父节点直接指向树根
* find_set 包含“压缩路径”优化
*/int find_set(vector<int>& parent, int x){if(parent[x] != x){parent[x] = find_set(parent, parent[x]);}return parent[x];}void link_set(vector<int>& parent, int rootA, int rootB){if(rank[rootA] > rank[rootB]){//假如A的高度比B高,则B的根节点指向A的根节点parent[rootB] = rootA;}else{parent[rootA] = rootB;if(rank[rootA] == rank[rootB]){++rank[rootB];}}}void union_set(vector<int>& parent, int x, int y){int rootA = find_set(parent, x);int rootB = find_set(parent, y);if(rootA == rootB){return;}link_set(parent, rootA, rootB);}
};

当然更容易想到的是BFS,每次BFS都会遍历一个连通分量,也就是题目中的省份,设置一个计数器,计算执行了多少次BFS就能得出省份数量。时间复杂度为O(n^2)

附BFS代码

class Solution {
public:vector<bool> isVisited;queue<int> queue;int findCircleNum(vector<vector<int>>& isConnected) {//思路1:使用BFS对图进行遍历,每次BFS的执行都会遍历完一个省份(连通分量),设置一个计数器计算使用了多少次BFS即可。时间复杂度为O(n^2)return BFS_Traversal(isConnected);}int BFS_Traversal(vector<vector<int>>& isConnected) {int province_num = 0;int verNum = isConnected.size();//图顶点的数量isVisited.resize(verNum, false);for(int i = 0;i < verNum;++i){//从0号顶点开始遍历if(!isVisited[i]){BFS(isConnected, i);++province_num;}}return province_num;}void BFS(vector<vector<int>>& isConnected, int i){int size = isConnected.size();//visitisVisited[i] = true;queue.push(i);while(!queue.empty()){int v = queue.front();queue.pop();for(int i = 0;i < size;++i){if(v != i && isConnected[v][i] != 0 && !isVisited[i]){//visitisVisited[i] = true;queue.push(i);}}}}};

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

easyExcel 导入、导出Excel 封装公共的方法

文档包含三部分功能 1、easyExcel 公共导出list<对象>方法&#xff0c;可以自定义excel中第一行和样式 2、easyExcel 导入逻辑&#xff0c;结合spring Validator 验证导入数据是否符合规范 3、easyExcel 自定义导出 list<map> 、 list<对象> &#xff08;可…

【论文阅读】Improved Denoising Diffusion Probabilistic Models

Improved Denoising Diffusion Probabilistic Models 文章目录 Improved Denoising Diffusion Probabilistic Models概述Improving the Log-likelihoodLearning ∑ θ ( x t , t ) \sum_{\theta}(x_{t}, t) ∑θ​(xt​,t)Improving the Noise ScheduleReducing Gradient Nois…

Kotlin 中List,Set,Map的创建与使用

目录 1. List 的使用 1.1 不可变 List 1.2 可变 List 2. Set 的使用 2.1 不可变 Set 2.2 可变 Set 3. Map 的使用 3.1 不可变Map 3.2 可变Map 本篇主要为已经有Java基础的同学展示Kotlin语言中的List&#xff0c;Set&#xff0c;Map的创建和使用&#xff0c;所以Java代…

【小白刷leetcode】第15题

【小白刷leetcode】第15题 动手刷leetcode&#xff0c;正在准备蓝桥&#xff0c;但是本人算法能力一直是硬伤。。。所以做得一直很痛苦。但是不熟练的事情像练吉他一样&#xff0c;就需要慢速&#xff0c;多练。 题目描述 看这个题目&#xff0c;说实在看的不是很懂。索性我们直…

uniapp 对video视频组件嵌套倍速按钮

这次接了需求是要求有倍速功能&#xff0c;去看了文档发现并没有倍速按钮的属性&#xff0c;想着手写一个吧 可最后发现原生层级太高&#xff0c;无论怎么样都迭不上去&#xff0c;就只能去找插件看看咯 找了好多插件发现都不可用&#xff0c;因为我这是app端&#xff0c;有些视…

Vue组件中引入jQuery

两种在vue中引入jQuery的方式 1、普通html中使用jQuery 将jQuer的文件导入到项目中&#xff0c;然后直接使用<script src"jQuery.js"></script>即可。 <script src"jQuery.js"></script> 2、vue组件中使用jQuery 安装依赖 c…

C语言数据结构基础笔记——树、二叉树简介

1.树 树是一种 非线性 的数据结构&#xff0c;它是由 n &#xff08; n>0 &#xff09;个有限结点组成一个具有层次关系的集合。 把它叫做树是因 为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 &#xff08;图片来源于网络&#xff09;…

【OJ】string类题目

个人主页 &#xff1a; zxctscl 如有转载请先通知 题目 1. 415字符串相加1.1 分析1.2 代码 2. 344反转字符串2.1 分析2.2 代码 3. HJ1字符串最后一个单词的长度3.1 分析3.2 代码 4. 387.字符串中的第一个唯一字符4.1 分析4.2 代码 5. 125验证回文串5.1 分析5.2 代码 1. 415字符…

wordpress被恶意搜索攻击(网址/?s=****)解决方法。

源地址&#xff1a;https://www.ctvol.com/seoomethods/1413686.html 什么叫恶意搜索攻击&#xff1f; wordpress恶意搜索攻击并不是像病毒一样的攻击&#xff0c;而是一种seo分支黑帽手段&#xff0c;通过被攻击网站搜索功能中长尾关键词来实现攻击&#xff0c;通过网址不断…

【LeetCode热题100】146. LRU 缓存(链表)

一.题目要求 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存中&#xff0c;则返回关键字的值&#xff0c…

Jenkins插件Parameterized Scheduler用法

Jenkins定时触发构建的同时设定参数。可以根据不同的定时构建器设置不同参数或环境变量的值。可以设置多个参数。并结合when控制stage流程的执行。结合when和triggeredBy区分定时构建的stage和手动执行的stage。 目录 什么是Parameterized Scheduler&#xff1f;如何配置实现呢…

代码随想录训练营Day24:● 理论基础 ● 77. 组合

理论基础 回溯算法解决的问题 回溯法&#xff0c;一般可以解决如下几种问题&#xff1a; 组合问题&#xff1a;N个数里面按一定规则找出k个数的集合 切割问题&#xff1a;一个字符串按一定规则有几种切割方式 子集问题&#xff1a;一个N个数的集合里有多少符合条件的子集 排列…

yolo项目中如何训练自己的数据集

1.收集自己需要标注的图片 2.打开网站在线标注网站 2.1 点击右下角Get Start 2.2点击这里上传自己的图片 上传成功后有英文的显示 点击左边的Object Detection&#xff0c;表示用于目标检测 2.3选择新建标签还是从本地加载标签 如果是本地加载标签&#xff08;左边&#…

基本常用函数help()

Python内置函数 help()函数&#xff1a;查看对象的帮助信息 print()函数&#xff1a;用于打印输出 input()函数&#xff1a;根据输入内容返回所输入的字符串类型 format()函数&#xff1a;格式化显示 len()函数&#xff1a;返回对象的长度或项目个数 slice()函数&#xf…

26-Java访问者模式 ( Visitor Pattern )

Java访问者模式 摘要实现范例 访问者模式&#xff08;Visitor Pattern&#xff09;使用了一个访问者类&#xff0c;它改变了元素类的执行算法&#xff0c;通过这种方式&#xff0c;元素的执行算法可以随着访问者改变而改变访问者模式中&#xff0c;元素对象已接受访问者对象&a…

TouchGFX之MVP

TouchGFX用户接口遵循Model-View-Presenter&#xff08;MVP&#xff09;架构模式&#xff0c;它是Model-View-Controller&#xff08;MVC&#xff09;模式的派生模式。 两者都广泛用于构建用户接口应用。 MVP模式的主要优势是&#xff1a; 关注点分离&#xff1a;将代码分成不…

mysql 排序底层原理解析

前言 本章详细讲下排序&#xff0c;排序在我们业务开发非常常见&#xff0c;有对时间进行排序&#xff0c;又对城市进行排序的。不合适的排序&#xff0c;将对系统是灾难性的&#xff0c;这个不是危言耸听。可能有些人会想&#xff0c;对于排序mysql 是怎么实现的&#xff0c;…

Android 地图SDK 绘制点 删除 指定

问题 Android 地图SDK 删除指定绘制点 详细问题 笔者进行Android 项目开发&#xff0c;对于已标记的绘制点&#xff0c;提供撤回按钮&#xff0c;即删除绘制点&#xff0c;如何实现。 解决方案 新增绘制点 private List<Marker> markerList new ArrayList<>…

Oracle数据库:使用 bash脚本 + 定时任务 自动备份数据

Oracle数据库&#xff1a;使用 bash脚本 定时任务 自动备份数据 1、前言2、为什么需要自动化备份&#xff1f;3、编写备份脚本4、备份脚本授权5、添加定时任务6、重启 crond / 检查 crond 服务状态7、备份文件检查 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收…

AI赋能写作:AI大模型高效写作一本通

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…