并查集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代…

CentOS安装MySQL详细教程

1.下载 MySQL yum包 wget http://repo.mysql.com/mysql57-community-release-el7-10.noarch.rpm 2.安装MySQL源 rpm -Uvh mysql57-community-release-el7-10.noarch.rpm 3.安装MySQL服务端 yum install -y mysql-community-server 4.启动MySQL systemctl start mysqld.service …

flink重温笔记(十八): flinkSQL 顶层 API ——时态表实现表数据动态变化(涵盖全面实用的 API )

Flink学习笔记 前言&#xff1a;今天是学习 flink 的第 18 天啦&#xff01;很多小伙伴私信说&#xff0c;自己只会SQL语法来编写flinkSQL&#xff0c;如何使用代码来操作呢&#xff1f;因为工作中都是要用到代码编写的。还有小伙伴说&#xff0c;想要实现表是动态变化的&#…

【小白刷leetcode】第15题

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

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

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

mysql笔记:15. 事务和锁

文章目录 一、事务概述二、事务基本操作三、事务保存点四、事务的隔离级别1. READ UNCOMMITTED设置事务的隔离级别 2. READ COMMITTED3. REPEATABLE READ4. SERIALIZABLE 五、MySQL的锁InnoDB的锁类型1. InnoDB的行级锁2. InnoDB的表级锁 死锁 在开发过程中&#xff0c;我们经常…

配置服务器SSH

在终端中&#xff0c;运行以下命令以检查SSH服务器的状态&#xff1a; sudo service ssh status安装SSH服务器。您可以运行以下命令来安装OpenSSH服务器&#xff0c;这是SSH服务的一个流行实现&#xff1a; sudo apt install openssh-server如果SSH服务器正在运行&#xff0c…

Acwing100 --- 增减序列(差分)

给定一个长度为 n 的数列 a1,a2,…,an&#xff0c;每次可以选择一个区间 [l,r]&#xff0c;使下标在这个区间内的数都加一或者都减一。 求至少需要多少次操作才能使数列中的所有数都一样&#xff0c;并求出在保证最少次数的前提下&#xff0c;最终得到的数列可能有多少种。 输入…

记录些实际应用开发过程中的prompt

Text2SQL 假设你是{dbType}的专家&#xff0c;需要通过问题描述和指令语句两部分内容帮忙生成对应查询SQL语句。第一部分问题说明&#xff1a; {queryContent} 第二部分指令内容&#xff1a; 1&#xff0c;不能幻觉出现新的字段&#xff0c;schema字段、表名称、表字段名称必须…

Vue组件中引入jQuery

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

KGCN---pytorch代码(2)---aggregator

代码&#xff1a; import torch import torch.nn.functional as Fclass Aggregator(torch.nn.Module):Aggregator classMode in [sum, concat, neighbor]#最后一个 neighbor 的聚合器直接就是利用邻域表示来代替 v 结点的表示def __init__(self, batch_size, dim, aggregator)…

vue组件基础及注册

1、组件的命名 kebab-case&#xff08;短横线&#xff09;命名法&#xff1a;字母全小写且必须包含一个连字符&#xff1b;例&#xff1a;my-component-namePascalCase&#xff08;帕斯卡&#xff09;命名法&#xff1a;首字符大写&#xff1b;例&#xff1a;MyComponentName …

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字符…

【python小技能】使用Python发送电子邮件的完整指南(适合零基础)

前言 在现代通信中&#xff0c;电子邮件是一种不可或缺的工具。使用Python编程语言&#xff0c;我们可以轻松地编写代码来发送电子邮件。本文将为零基础的读者提供一个完整的指南&#xff0c;教你如何使用Python发送电子邮件 安装库 首先&#xff0c;我们需要安装smtplib库。…

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

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

Clickhouse MergeTree原理(二)—— 表和分区的维护

作者&#xff1a;俊达 引言 MergeTree是Clickhouse中最核心的存储引擎。上一篇文章中&#xff0c;我们介绍了MergeTree的基本结构。 1、MergeTree由分区&#xff08;partiton&#xff09;和part组成。 2、Part是MergeTree可操作的基本数据单元。 当插入数据时&#xff0c;会…

MySQL 中的“两阶段提交”机制

在MySQL数据库中&#xff0c;为了确保redo log&#xff08;重做日志&#xff09;和binlog&#xff08;二进制日志&#xff09;之间的数据安全性和一致性&#xff0c;引入了“两阶段提交”这一重要概念。MySQL将redo log的写入过程细分为“prepare”和“commit”两个步骤&#x…