【算法】并查集的介绍与使用

news2025/1/8 12:35:11

1.并查集的概论

定义:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

主要构成:
并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。

作用:
并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)

2.用一个故事来引入并查集(“魔法学院的联盟”

在遥远的阿尔多王国,有一座声名远扬的魔法学院——天星学院。这所学院不仅教授强大的魔法,还拥有许多古老的传说。传说中提到,曾经有一个神秘的魔法师,名叫艾尔文,他发明了一种神奇的魔法工具,叫做“并查集”。

故事发生在天星学院的一个风和日丽的早晨,学院的学生们正忙于准备即将到来的年度魔法比赛。比赛的内容是让学生们组成小组进行各种挑战,争夺“学院最佳团队”的荣誉。

艾伦和他的朋友们被分配到了同一个小组,但在比赛前的准备中,他们发现学院里有多个小组已经相互合作,形成了若干个强大的联盟。这些联盟的存在让他们感到有些困惑,因为他们不知道如何将所有的合作伙伴都联系起来,从而提高自己的团队实力。

就在他们为此感到苦恼时,学院的长者向他们介绍了一种古老的魔法——并查集。长者解释说,并查集是一种强大的魔法工具,可以帮助他们轻松地管理和合并不同的联盟。

艾伦和他的朋友们认真听取了长者的讲解,并决定尝试使用这种魔法。他们发现,并查集的核心在于两个主要操作:

  1. 合并(Union):当两个小组决定合作时,他们可以通过这个操作将两个小组合并成一个更强大的联盟。
  2. 查找(Find):当他们需要知道某个小组是否已经与其他小组在同一个联盟中时,可以使用这个操作来检查。

使用了并查集的魔法后,艾伦和他的团队发现管理各个小组变得更加简单。他们能够迅速地找到已经存在的联盟,并根据需要进行合并。这使得他们能够更好地协调和组织自己的资源,形成了一个强大的联盟。

随着比赛的进行,艾伦和他的团队利用并查集的魔法成功地将所有的小组整合到一起,形成了一个无敌的联盟。他们在比赛中表现出色,最终赢得了“学院最佳团队”的荣誉。

在获胜的庆祝会上,艾伦和他的朋友们感慨万千,他们深刻理解了并查集魔法的强大力量,也明白了团队合作的重要性。从那以后,天星学院的学生们都学会了如何使用并查集来解决各种复杂的联盟问题,而艾伦和他的团队也因为这次经历成为了学院的传奇人物。

3.从数据结构的方向看并查集

  1. 查找(Find):确定一个元素属于哪个集合,通常返回集合的代表元素或根节点。此操作可以通过路径压缩优化,使得树的高度保持较小,从而提高查找效率。

  2. 合并(Union):将两个集合合并为一个集合。合并操作可以通过按秩合并(按树的深度合并)或按大小合并(合并小的树到大的树)来优化,从而保持数据结构的效率。

并查集常用于处理动态连通性问题,例如在图论中的连通分量计算。通过这些操作,并查集能够在接近常数时间内完成集合的合并和查找。

3.find()函数的定义与实现

find() 函数用于确定一个元素所在的集合的代表元素或根节点。它通常通过路径压缩优化来提高效率。

开始时每个集合都是一个独立的集合,并且都是等于自己本身下标的数
例如:
p[5]=5,p[3]=3;


如果是M操作的话那么就将集合进行合并,合并的操作是:
p[3]=p[5]=5;
所以3的祖宗节点便成为了5
此时以5为祖宗节点的集合为{5,3}
如果要将p[9]=9插入到p[3]当中,应该找到3的祖宗节点,
然后再把p[9]=9插入其中,所以p[9]=find(3);(find()函数用于查找祖宗节点)
也可以是p[find(9)]=find(3),因为9的节点本身就是9
此时以5为祖宗节点的集合为{5,3,9};
如果碰到多个数的集合插入另一个集合当中其原理是相同的


例如:
上述中以5为祖宗节点的是p[5],p[3],p[9];(即p[5]=5,p[3]=5,p[9]=5)
再构造一个以6为祖宗节点的集合为{6,4,7,10}
如果要将以6为祖宗节点的集合插入到以5为祖宗节点的集合,则该操作可以是
p[6]=find(3)(或者find(9),find(5))
此时p[6]=5
当然如果是以6为祖宗节点集合中的4,7,10则可以这样
p[find(4)]=find(3)
或者p[find(7)]=find(3)均可以
此时以6为祖宗节点的集合的祖宗节点都成为了5

3.2find()函数的实现

第一个find函数同时也运用了状态压缩即:

find 函数不仅有找祖宗的功能,还把这个查找路径上所有节点直接变成了祖宗节点的孩子

int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    /*
    经上述可以发现,每个集合中只有祖宗节点的p[x]值等于他自己,即:
    p[x]=x;
    */
    return p[x];
    //找到了便返回祖宗节点的值
}

2.
int find(int x)					//查找x的祖宗结点
{
	while(pre[x] != x)			//如果x的上级不是祖宗节点
		x = pre[x];				//x继续找
	return x;					
}

4.合并集合的代码实现与逻辑

  • 先查找两个元素 x 和 y 的根节点。
  • 比较两个根节点的秩(高度),将秩较小的根节点的树合并到秩较大的根节点的树下,确保树的高度尽可能低。
  • 如果两个根节点的秩相同,将其中一个根节点设置为另一个根节点的子节点,并将该根节点的秩加一。
//合并a b所在的两个集合
void merge(int a, int b)
{
    int pa = find(a);//找到 a 所在集合的代表元素
    int pb = find(b);//找到 b 所在集合的代表元素
    if(pa != pb)//如果不是同一个,则属于不同集合,需要合并
    {
        p[pa] = pb;//将a所在集合代表元素的代表元素设置为b所在集合的代表元素。
    }
}

5.并查集的测试代码(进行了按秩合并的写法)

#include <vector>
#include <iostream>

class DisjointSet {
public:
    DisjointSet(int size) : parent(size), rank(size, 1) {
        // 初始化每个元素的父节点指向自己
        for (int i = 0; i < size; ++i) {
            parent[i] = i;
        }
    }

    // 查找元素 x 所在集合的根节点,并进行路径压缩
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // 路径压缩
        }
        return parent[x];
    }

    // 合并两个集合
    void unionSets(int x, int y) {
        int rootX = find(x); // 查找 x 的根节点
        int rootY = find(y); // 查找 y 的根节点

        if (rootX != rootY) {
            // 按秩合并:将秩较低的树合并到秩较高的树下
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX] += 1;
            }
        }
    }

private:
    std::vector<int> parent; // 记录每个元素的父节点
    std::vector<int> rank;   // 记录每个树的秩
};

int main() {
    DisjointSet ds(10); // 初始化一个包含10个元素的并查集

    ds.unionSets(1, 2); // 合并集合 {1} 和 {2}
    ds.unionSets(2, 3); // 合并集合 {1, 2} 和 {3}
    ds.unionSets(4, 5); // 合并集合 {4} 和 {5}

    // 检查合并后的结果
    std::cout << "Find 1: " << ds.find(1) << std::endl; // 输出 1
    std::cout << "Find 3: " << ds.find(3) << std::endl; // 输出 1
    std::cout << "Find 4: " << ds.find(4) << std::endl; // 输出 4
    std::cout << "Find 5: " << ds.find(5) << std::endl; // 输出 4

    ds.unionSets(3, 5); // 合并集合 {1, 2, 3} 和 {4, 5}

    // 检查合并后的结果
    std::cout << "Find 5 after union with 3: " << ds.find(5) << std::endl; // 输出 1,因为 1 和 5 现在在同一集合中

    return 0;
}

6.简单的实现代码(没有使用按秩合并的写法)

#include <vector>
#include <iostream>

class DisjointSet {
public:
    DisjointSet(int size) : parent(size) {
        for (int i = 0; i < size; ++i) {
            parent[i] = i; // 每个元素的父节点初始化为自身
        }
    }

    // 查找元素 x 所在集合的根节点,并进行路径压缩
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // 路径压缩
        }
        return parent[x];
    }

    // 合并 a 和 b 所在的两个集合
    void merge(int a, int b) {
        int rootA = find(a); // 查找 a 所在集合的代表元素
        int rootB = find(b); // 查找 b 所在集合的代表元素

        if (rootA != rootB) {
            parent[rootB] = rootA; // 直接将 b 的根节点挂到 a 的根节点下
        }
    }

private:
    std::vector<int> parent; // 记录每个元素的父节点
};

int main() {
    DisjointSet ds(10); // 初始化一个包含10个元素的并查集

    ds.merge(1, 2); // 合并集合 {1} 和 {2}
    ds.merge(2, 3); // 合并集合 {1, 2} 和 {3}
    ds.merge(4, 5); // 合并集合 {4} 和 {5}

    // 检查合并后的结果
    std::cout << "Find 1: " << ds.find(1) << std::endl; // 输出 1
    std::cout << "Find 3: " << ds.find(3) << std::endl; // 输出 1
    std::cout << "Find 4: " << ds.find(4) << std::endl; // 输出 4
    std::cout << "Find 5: " << ds.find(5) << std::endl; // 输出 4

    ds.merge(3, 5); // 合并集合 {1, 2, 3} 和 {4, 5}

    // 检查合并后的结果
    std::cout << "Find 5 after union with 3: " << ds.find(5) << std::endl; // 输出 1,因为 1 和 5 现在在同一集合中

    return 0;
}

7.一些基本操作的实现

const int  N=1005					//指定并查集所能包含元素的个数(由题意决定)
int pre[N];     					//存储每个结点的前驱结点 
int rank[N];    					//树的高度 
void init(int n)     				//初始化函数,对录入的 n个结点进行初始化 
{
    for(int i = 0; i < n; i++){
        pre[i] = i;     			//每个结点的上级都是自己 
        rank[i] = 1;    			//每个结点构成的树的高度为 1 
    } 
}
int find(int x)     	 		    //查找结点 x的根结点 
{
    if(pre[x] == x) return x;  		//递归出口:x的上级为 x本身,则 x为根结点 
    return find(pre[x]); 			//递归查找 
} 
 
int find(int x)     				//改进查找算法:完成路径压缩,将 x的上级直接变为根结点,那么树的高度就会大大降低 
{
    if(pre[x] == x) return x;		//递归出口:x的上级为 x本身,即 x为根结点 
    return pre[x] = find(pre[x]);   //此代码相当于先找到根结点 rootx,然后 pre[x]=rootx 
} 

bool isSame(int x, int y)      		//判断两个结点是否连通 
{
    return find(x) == find(y);  	//判断两个结点的根结点(即代表元)是否相同 
}

bool join(int x,int y)
{
    x = find(x);						//寻找 x的代表元
    y = find(y);						//寻找 y的代表元
    if(x == y) return false;			//如果 x和 y的代表元一致,说明他们共属同一集合,则不需要合并,返回 false,表示合并失败;否则,执行下面的逻辑
    if(rank[x] > rank[y]) pre[y]=x;		//如果 x的高度大于 y,则令 y的上级为 x
    else								//否则
    {
        if(rank[x]==rank[y]) rank[y]++;	//如果 x的高度和 y的高度相同,则令 y的高度加1
        pre[x]=y;						//让 x的上级为 y
	}
	return true;						//返回 true,表示合并成功
}

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

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

相关文章

three.js的粒子和粒子系统基础知识扫盲,附案例图

绚烂的烟花、急促的雨滴、深邃的宇宙等等这些效果都可以通过three.js的粒子效果模拟出来&#xff0c;已达到以假乱真的程度了&#xff0c;本文来分享一下three.js的粒子系统&#xff0c;欢迎大家点赞评论收藏。 一、什么是粒子和粒子系统 粒子&#xff1a;可以简单理解为一个具…

JDBC1 Mysql驱动,连接数据库

JDBC 一、JDBC Java Database Connectivity&#xff1a;Java访问数据库的解决方案 JDBC定义了一套标准接口&#xff0c;即访问数据库的通用API&#xff0c; 不同的数据库厂商根据各自数据库的特点去实现这些接口。 JDBC希望用相同的方式访问不同的数据库&#xff0c;让具体的…

集成OpenFeign和Sentinel实现自定义服务降级Fallback及网关流量控制

文章目录 sentinel下载安装启动sentinel访问sentinelpmhub-gateway 整合 sentinel引入依赖YML配置文件Nacos持久化配置 启动pmhub-gateway, 查看sentinel控制台启动结果如图相关名词解释 OpenFeign和 Sentinel 集成实现自定义 fallback 服务降级 Sentinel 和 Gateway 集成实现网…

苹果在iOS 18.1中向第三方开发者开放iPhone的NFC芯片

苹果公司今天宣布&#xff0c;开发者很快就能首次在自己的应用程序中提供 NFC 交易功能&#xff0c;而目前这主要是Apple Pay独有的功能。从今年晚些时候的 iOS 18.1 开始&#xff0c;开发者将可以使用新的 API 提供独立于 Apple Pay 和 Apple Wallet 的应用内非接触式交易。 这…

多线程之并发锁

多线程之并发锁 Synchronized 特性&#xff1a; 可重入&#xff0c;持有该锁的线程可以再次获取锁不可中断&#xff1a;获取了Synchronized锁之后就必须要等其释放锁&#xff0c;响应不了中断灵活性不高&#xff1a;使用Synchronized锁只能是进入到代码块内执行完了才释放锁…

基于DPU云盘挂载的Spark优化解决方案

1. 方案背景和挑战 Apache Spark&#xff0c;作为当今大数据处理领域的佼佼者&#xff0c;凭借其高效的分布式计算能力、内存计算优化以及强大的生态系统支持&#xff0c;已牢固确立其在业界的标杆地位。Spark on Kubernetes&#xff08;简称K8s&#xff09;作为Spark与Kuber…

代码随想录训练营 Day30打卡 贪心算法 part04 452. 用最少数量的箭引爆气球 435. 无重叠区间 763. 划分字母区间

代码随想录训练营 Day30打卡 贪心算法 part04 一、 力扣452. 用最少数量的箭引爆气球 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points &#xff0c;其中points[i] [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球…

YoloV8改进策略:Block改进|LeYOLO,一种用于目标检测的新型可扩展且高效的CNN架构|复现LeYolo,轻量级Yolo改进

摘要 在目标检测中&#xff0c;深度神经网络的计算效率至关重要&#xff0c;尤其是随着新型模型越来越注重速度而非有效计算量&#xff08;FLOP&#xff09;。这一发展趋势在某种程度上忽视了嵌入式和面向移动设备的AI目标检测应用。在本文中&#xff0c;我们基于FLOP关注于高…

热泵干燥应用举例

热泵在木材加工中的应用主要是热泵干燥&#xff0c;具有能耗低、干燥质量好等特点。热泵木材干燥装置的基本结构是封闭式干燥窑&#xff0c;其中热泵机组的结构有单热源型&#xff08;图18-4&#xff09;、双热源型&#xff08;图18-5&#xff09;和空气回热型&#xff08;图18…

深入理解Java中的ConcurrentHashMap:高效线程安全的并发容器

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

Java并发:内存屏障,Atomic类,CAS函数,伪共享

阅读本文之前可以看一看 Java 多线程基础&#xff1a; Java&#xff1a;多线程&#xff08;进程线程&#xff0c;线程状态&#xff0c;创建线程&#xff0c;线程操作&#xff09; Java&#xff1a;多线程&#xff08;同步死锁&#xff0c;锁&原子变量&#xff0c;线程通信&…

【学习笔记】A2X通信的协议(十)- 通过PC5的直接探测与避让(DDAA)

3GPP TS 24.577 V18.1.0的技术规范&#xff0c;主要定义了5G系统中A2X通信的协议方面&#xff0c;特别是在PC5接口和Uu接口上的A2X服务。以下是文件的核心内容分析&#xff1a; 8. 通过PC5的直接探测与避让&#xff08;DDAA&#xff09; 8.1 概述 本条款描述了UE之间以及UE上…

论文阅读:Efficient Core Maintenance in Large Bipartite Graphs | SIGMOD 2024

还记得我们昨天讨论的《Querying Historical Cohesive Subgraphs over Temporal Bipartite Graphs》这篇论文吗? https://blog.csdn.net/m0_62361730/article/details/141003301 这篇(还没看的快去看) 这篇论文主要研究如何在时间双向图上查询历史凝聚子图&#xff0c;而《E…

CAD二次开发IFoxCAD框架系列(18)-块表操作

1. 块表的查询 1.1 查找名为“自定义块”的块表中的图块记录 using var tr new DBTrans(); if (tr.BlockTable.Has("自定义块")) {//要执行的操作 }遍历块表并打印所有的块表的图块名称 public void Test_DBTrans_BlockCount() {using var tr new DBTrans();var…

CentOS7.9上通过KVM安装Centos虚拟机

目录 1 开发前准备&#xff08;先确保服务器可以虚拟化&#xff09;&#xff1a; 2、安装KWM环境 3、创建镜像文件存放目录 4、创建镜像文件存放目录 5、安装桥连接虚拟网络 6、安装虚拟机 7、配置操作系统 8、虚拟机配置网卡地址 9、克隆虚拟机执行 1开发前准备&am…

Git文件管理技巧:轻松删除与查看文件,忽略不必要的文件与文件夹!

避免文件混乱&#xff1a;Git 文件操作技巧 一、Git工作原理概述二、删除文件三、查看指定文件的修改四、指定不需要 Git 管理的文件五、总结 一、Git工作原理概述 Git是一种分布式版本控制系统&#xff0c;其核心在于其高效的快照机制、强大的分支与合并功能、本地开发的灵活…

数据集与数据库:有什么区别?

数据集和数据库是我们在处理数据时经常听到的两个常用词。虽然它们听起来很相似&#xff0c;但它们具有不同的特征并用于不同的用途。本文深入探讨数据集和数据库之间的主要区别&#xff0c;探索了它们的结构、数据类型和各种其他功能&#xff0c;以帮助您做出明智的决定&#…

回归预测|基于灰狼优化GWO-Transformer-BiLSTM组合模型的数据回归预测Matlab程序 多特征输入单输出

回归预测|基于灰狼优化GWO-Transformer-LSTM组合模型的数据回归预测Matlab程序 多特征输入单输出 文章目录 前言回归预测|基于灰狼优化GWO-Transformer-BiLSTM组合模型的数据回归预测Matlab程序 多特征输入单输出GWO-Transformer-BiLSTM 一、GWO-Transformer-BiLSTM模型二、实验…

uniapp打包H5的时候 清楚缓存(不安装依赖的前提下)

问题 在写项目的时候&#xff0c;打包好一个H5 发布成功&#xff0c;后来又重新打包新的包进行更新迭代&#xff0c;但是用户手机上还是上一个版本&#xff0c;本地缓存还是没有清除。 解决问题 步骤一&#xff1a;html不缓存 在html中&#xff0c;解决缓存的方法主要是依赖…

文章解读与仿真程序复现思路——电力自动化设EI\CSCD\北大核心《海上风电全直流汇集送出系统自适应振荡抑制策略》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…