并查集详解

并查集 文章目录一、并查集定义二、并查集思想三、并查集代码:(1)初始化(2)查找(3)合并四、路径压缩 一、并查集定义 1. 并查集是一种树型的数据结构,用于处理一些不相交集合(disjoint sets)的合并及查询问题。 2. 并查集通常包含两种操作 查找(Find):查询两个元素是否在同一个集合中 合并(Union):把两个不相交的集合合并为一个集合 注意:双亲结点就是父结点 二、并查集思想 如图现在大陆上有下面六位鼎鼎大名的忍者,且各自为王!     & 阅读详情

什么是并查集?

并查集集是一种维护集合的数据结构,它的名字中 “并”、“查”、“集” 分别取自 Union(合并)、Find(查找)、Set(集合)这 3 个单词。

也就是说,并查集支持下面两个操作:

  1. 合并:合并两个集合。
  2. 查找:判断两个元素是否在一个集合。

那么并查集是用什么实现的呢?

其实就是用一个数组:

int father[n];

其中 father[i] 表示元素 i 的父亲结点,而父亲结点本身也是这个集合内的元素。

例如 father[1] = 2 就表示元素 1 的父亲结点是元素 2 ,以这种父系关系来表示元素所属的集合。

另外,如果 father[i] = i ,则说明元素 i 是该集合的根结点,但对同一个集合来说只存在一个根结点,且将其作为所属集合的标识。

在这里插入图片描述

举个例子,下面给出了上图的 father 数组情况。

father[1] = 1; 	// 1 的父亲结点是自己,也就是说 1 号是根结点
father[2] = 1;	// 2 的父亲结点是 1
father[3] = 2;	// 3 的父亲结点是 2  
father[4] = 2;	// 4 的父亲结点是 2
father[5] = 5;	// 5 的父亲结点是自己,也就是说 5 号是根结点
father[6] = 5;	// 6 的父亲结点是 5

我们发现有这样的规律:

father[i] = 1 说明元素 1 的父亲结点是自己,即元素 1 是集合的根结点。

father[2] = 1 说明元素 2 的父亲结点是元素 1 ,

father[3] = 2 和 father[4] = 2 说明元素 3 和 元素 4 的父亲结点都是元素 2 ,

这样元素 1、2、3、4 就在同一个集合当中。

fathe[5] = 5 和 father[6] = 5 则说明 5 和 6 是以 5 为根结点的集合。

这样就得到了两个不同的集合。

并查集的基本操作

总体来说,并查集的使用需要先初始化 father 数组,然后再根据需要进行查找或合并的操作。

初始化

一开始,每个元素都是独立的一个集合,因此需要令所有 father[i] 等于 i :

void init()
{
    // iota函数可以把数组初始化为 0 到 n-1
    iota(father.begin(), father.end(), 0);
}

查找根结点

由于规定同一个集合中只存在一个根结点,因此查找操作就是对给定的结点寻找其根结点的过程。

实现的方式可以是迭代或是递归,但是其思路都是一样的,即反复寻找父亲结点,直到找到根结点(即 father[i] = i 的结点)。

迭代:

int find(int x)
{
    while (x != father[x])
    {
        // 路径压缩,使得下次查找更快
        father[x] = father[father[x]];
        x = father[x];
    }
    return x;
}

在这里插入图片描述

以上图为例,要查找元素 4 的根结点是谁,应按照上面的递推方法,流程如下:

  1. x = 4 , father[4] = 2,因此 4 != father[4],于是继续查;

  2. x = 2, father[2] = 1,因此 2 != father[2],于是继续查;

  3. x = 1, father[1] = 1,因此 1 = father[1] ,找到根结点,返回 1 。

当然,这个过程也可以用递归来实现:

int find(int x)
{
    if (x == father[x])
        return x; // 如果找到根结点,则返回根结点编号x
    else
        return find(father[x]); // 否则,递归判断 x 的父亲结点是否是根结点
}

递归(one line code):

int find(int x)
{
    return father[i] == i ? father[i] : find(father[i]);
}

查询两个结点是否同根

当我们要查询两个元素是否属于同一个组时,我们需要沿着各个节点往上向树的根进行查询,如果最终发现两个元素的根相同,那么他们就属于同一个组。反之,则不属于同一个组。

代码如下所示:

bool isConnected(int p, int q)
{
    return find(p) == find(q);
}

合并

合并是指把两个集合合并成一个集合,题目中一般给出两个元素,要求把这两个元素所在的集合合并。

具体实现上一般是先判断两个元素是否属于同一个集合,只有当两个元素属于不同集合时才合并,而合并的过程一般是把其中一个集合的根结点的父亲指向另一个集合的根结点。

于是思路就比较清晰了,主要分为以下两步:

  1. 对于给定的两个元素 a、b,判断它们是否属于同一集合。可以调用上面的查找函数,对这两个元素 a、b 分别查找根结点,然后再判断其根结点是否相同。

  2. 合并两个集合:在 步骤1 中已经获得了两个元素的根结点 faA 与 faB ,因此只需要把其中一个的父亲结点指向另一个结点。例如可以令 father[faA] = faB,当然反过来令 father[faB] = faA 也是可以的,两者没有区别。

还是以下图为例:

在这里插入图片描述

把元素 4 和元素 6 合并,过程如下:

  1. 判断元素 4 和元素 6 是否属于同一个集合:元素 4 所在集合的根结点是 1 ,元素 6 所在集合的根结点是 5 ,因此它们不属于同一个集合。

  2. 合并两个集合:令 father[5] = 1 ,即把元素 5 的父亲设为元素 1 。

于是有了合并后的集合,如图所示:

在这里插入图片描述

现在可以写出合并的代码了:

void connect(int a, int b)
{
    int faA = find(a); // 查找 a 的根结点,记为 faA
    int faB = find(b); // 查找 b 的根结点,记为 faB
    if (faA != faB)
    {
        // 如果不属于同一个集合
        father[faA] = faB; // 合并它们
    }
}

这里需要注意的是,很多初学者会直接把其中一个元素的父亲设为另一个元素,即直接令 father[a]=b 来进行合并,这并不能实现将集合合并的效果。

例如,将上面例子中的 father[4] 设为 6 ,或是把 father[6] 设为 4 ,就不能实现集合合并的效果,如图所示。

在这里插入图片描述

因此,初学者使用上面给出的 connect 函数来进行合并操作。

最后说明并查集的一个性质。在合并的过程中,只对两个不同的集合进行合并,如果两个元素在相同的集合中,那么就不会对它们进行操作。这就保证了在同一个集合中一定不会产生环,即并查集产生的每一个集合都是一棵树。

并查集的优化

优化1:避免退化(按秩合并)

因为并查集的结构是树状结构,所以需注意退化问题。避免退化发生的方法如下:

首先,我们合并时,可记录这棵树的高度(记为 size)。

接下来当我们需合并两棵树时,我们先对两棵树的高度进行判断,如不同,则让高度小的树的根指向高度大的根。如下图:

在这里插入图片描述

优化代码如下所示:

    void init()
    {
        // iota函数可以把数组初始化为 0 到 n-1
        iota(father.begin(), father.end(), 0);
        for (int i = 0; i < size.size(); i++)
            size[i] = 1;
    }
void connect(int p, int q)
{
    int i = find(p), j = find(q);
    if (i != j)
    {
        // 按秩合并:每次合并都把深度较小的集合合并在深度较大的集合下面
        if (size[i] < size[j])
        {
            father[i] = j;
            size[j] += size[i];
        }
        else
        {
            father[j] = i;
            size[i] += size[j];
        }
    }
}

优化2:路径压缩

由于查询时我们需沿着元素所在的树从下往上查询,最终找到这棵树的根,表明这个元素与其根对应元素属于同一组。

因为在此查询过程中我们会经过许多节点,而如果我们能将这个元素直接指向根节点,那么就能节省许多查询的时间。

同时,在查询过程中,每次经过的节点,我们都可以同时将他们一起直接指向根节点。这样做的话,我们再查询这些节点时,就能很快知道他们的根是谁了。

在这里插入图片描述

迭代:

int find(int x)
{
    while (x != father[x])
    {
        // 路径压缩,使得下次查找更快
        father[x] = father[father[x]];
        x = father[x];
    }
    return x;
}

递归:

int find(int x)
{
    if (x == father[x])
        return x; // 如果找到根结点,则返回根结点编号x
    else
    {
        // 使元素直接指向树的根
        father[x] = find(father[x]);
        return find(father[x]); // 否则,递归判断 x 的父亲结点是否是根结点
    }
}

递归(one line code):

int find(int x)
{
    // 使元素直接指向树的根
    return father[x] == x ? father[x] : father[x] = find(father[x]);
}

复杂度分析

经过两个优化后,并查集的效率变得非常高。

对n个元素的并查集进行一次操作的均摊复杂度是O(α(n)) (α(n)是阿克曼函数的反函数,α(n) 可以认为是一个很小的常数),比优化前的O(log(n))还要快。

最终代码实现

class UnionFind
{
    vector<int> father, size;

public:
    UnionFind(int n) : father(n), size(n) {}
    void init()
    {
        // iota函数可以把数组初始化为 0 到 n-1
        iota(father.begin(), father.end(), 0);
        for (int i = 0; i < size.size(); i++)
            size[i] = 1;
    }
    int find(int x)
    {
        while (x != father[x])
        {
            // 路径压缩,使得下次查找更快
            father[x] = father[father[x]];
            x = father[x];
        }
        return x;
    }
    void connect(int p, int q)
    {
        int i = find(p), j = find(q);
        if (i != j)
        {
            // 按秩合并:每次合并都把深度较小的集合合并在深度较大的集合下面
            if (size[i] < size[j])
            {
                father[i] = j;
                size[j] += size[i];
            }
            else
            {
                father[j] = i;
                size[i] += size[j];
            }
        }
    }
    bool isConnected(int p, int q)
    {
        return find(p) == find(q);
    }
};

init 函数可以省略:

class UnionFind
{
    vector<int> father, size;

public:
    UnionFind(int n) : father(n), size(n, 1)
    {
        // iota函数可以把数组初始化为 0 到 n-1
        iota(father.begin(), father.end(), 0);
    }
    int find(int x)
    {
        while (x != father[x])
        {
            // 路径压缩,使得下次查找更快
            father[x] = father[father[x]];
            x = father[x];
        }
        return x;
    }
    void connect(int p, int q)
    {
        int i = find(p), j = find(q);
        if (i != j)
        {
            // 按秩合并:每次合并都把深度较小的集合合并在深度较大的集合下面
            if (size[i] < size[j])
            {
                father[i] = j;
                size[j] += size[i];
            }
            else
            {
                father[j] = i;
                size[i] += size[j];
            }
        }
    }
    bool isConnected(int p, int q)
    {
        return find(p) == find(q);
    }
};

例题

Leetcode684. 冗余连接

Leetcode1971. 寻找图中是否存在路径

并查集(标准并查集、带权并查集、扩展域并查集) 并查集是一种数据结构,用来管理集合之间的关系,可以用来进行判断是否属于同一集合或将不同的集合合并等操作。我们用个生活中的例子来说明一下原本小明家这个集合只有小明,小红家只有小红,那么他们结婚后(合并)就属于一个家庭(一个集合),像这样,通过并查集我们能够合并不同的集合,然后还能快速地查找他们是不是同一个集合。 阅读详情

相关推荐

并查集(算法+模板+讲解)

并查集(算法+模板+讲解)

数据结构(八):并查集详解 (多图+动图)

数据结构(八):并查集详解 (多图+动图)

ReadyGo!!!的博客 2万+

云漾AI创作平台实战:如何用GPT4.0+Midjourney快速生成商业级插画(附Prompt模板)

本文详细介绍了如何利用云漾AI创作平台结合GPT4.0和Midjourney快速生成商业级插画。通过智能文案生成与视觉化工作流,大幅提升创作效率,适用于电商、社交媒体等多种场景。文章包含实用Prompt模板和参数调整技巧,帮助设计师和内容创作者高效产出专业插画。

weixin_29251567的博客 223

【高级数据结构】并查集

前面讲解了并查集的基本应用—一处理集合问题。在这些基本应用中,点之间只有简单的归属关系,而没有权值。如果在点之间加上权值,并查集的应用会更广泛。如果读者联想到树这种数据结构,会发现并查集实际上是在维护若干棵树。并查集的合并和查询优化,实际上是在改变树的形状,把原来“细长”的,操作低效的大量“小树”,变为“粗短”的、操作高效的少量“大树”。如果在原来的“小树”上,点之间有权值,那么经过并查集的优化变成“大树”后,这些权值的操作也变得高效了。定义一个权值数组dd[]d,把节点iii。

Antonio915的博客 1万+

ROS入门(一)——ROS安装(vmware16+ubuntu20.04+ROS-Noetic)

嵌套:ROS可以在Ubuntu上运行,Ubuntu在虚拟机Vmware中安装。 从 虚拟机vmware安装→ubuntu安装→ros安装,逐步介绍安装ROS环境的过程。整个过程是我一步步跟着各种博客安装实现的,亲测可行。 版本: Vmware :Vmware Workstation16 Ubuntu :20.04 ROS :Noetic 其中vmware直接载当前最新版本就好,而ubuntu的版本要注意一下。 在ROS官网(cn - ROS Wiki),我们进入到ROS安装的步骤界面,可以看..

Netceor的博客 9742

不用Root也能抓包?Android高版本HTTPS抓包的3种替代方案(含Charles技巧)

本文针对Android高版本HTTPS抓包难题,提供了三种无需Root权限的实战解决方案:利用VirtualXposed等虚拟沙盒环境、配置Postern等系统级代理工具,以及通过修改应用源码或网络安全配置。文章还详细解析了Charles抓包工具在非Root环境下的高级配置与排错技巧,帮助开发者在遵守安全规范的前提下,有效进行网络调试与监控。

http9protocoller的博客 648

并查集之种类并查集——详解

知乎上看到的一篇文章,解释得非常到位了,有图有文字有题目 种类并查集详解

随意凯的博客 1059

37.华为路由器:loopback(本地回环)接口的作用

本地环回接口(或地址),亦称回送地址(loopback address)。 此类接口是应用最为广泛的一种虚接口,几乎在每台路由器上都会使用。常见于如下用途: 1 作为一台路由器的管理地址 系统管理员完成网络规划之后,为了方便管理,会为每一台路由器创建一个loopback 接口,并在该接口上单独指定一个IP 地址作为管理地址 ,管理员会使用该地址对路由器远程登录(telnet ),该地址实际上起到了类似设备名称一类的功能。 但是通常每台路由器上存在众多接口和地址,为何不从当中随便挑选一个呢 ? 原因如下:由于

迷逝 3万+

c++ 并查集详解

获取代表元素:获取某个集合的代表元素(根节点)。在并查集中,每个集合都有一个代表元素,该代表元素是该集合中任意元素的祖先节点。通过遍历该元素的父指针,可以找到其所在的集合的代表元素(根节点)。首先找到需要合并的两个集合的代表元素(根节点),然后将其中一个集合的代表元素的父指针指向另一个集合的代表元素,从而实现合并。路径压缩:在查找过程中,将某个元素的父指针直接指向其所在集合的根节点,以减少后续查找的时间复杂度。通过遍历该元素的父指针,可以找到其所在的集合的代表元素(根节点),从而判断该元素是否属于该集合。

weixin_72389218的博客 2583

基于STM32L4XX、HAL库的AD7768BSTZ 模数转换器ADC驱动程序设计

/ 默认SPI配置。// 读取通道数据命令。// 通过SYNC_IN引脚触发转换(如果使用)// 处理数据...// 或者通过SPI命令。// 初始化AD7768。// SPI1引脚配置。

zhusl6688的博客 462

【即插即用模块】注意力篇 | MICCAI 2023 | MFE:多尺度特征提取,告别固定感受野,猛涨2个点!

本文提出一种用于低分辨率乳腺癌病理图像超分辨率与分类的网络SHISRCNet。针对现有方法处理低分辨率图像效果差、感受野固定等问题,设计了多特征提取模块(MFE),通过多速率空洞卷积覆盖全尺度感受野(局部到全局),结合递进残差连接和动态权重融合机制,实现自适应多尺度特征增强。实验表明,该模块在超分辨率任务中PSNR提升近2个点,分类准确率显著提高。代码提供详细注释,支持即插即用,适用于各类多尺度视觉任务。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 207

带权并查集 详解

简介 带权并查集与普通并查集的区别在于,其每条边/每个点都有权值。因此,我们需要额外用一个数组来维护每个点 xxx 到其当前祖先 faxfa_xfax​ 的权值和,并在路径压缩的时候维护。 直接说可能较难理解,于是下面是一道例题。 P1169 Description 传送门 Solution 考虑对于每个点 xxx,维护其到其祖先 faxfa_xfax​ 的距离 depxdep_xdepx​,并对于每个并查集的根维护连通块大小 sizsizsiz。 对于一个 M 操作,令涉及到的战舰分别为 x,yx,yx,

Cherrt的博客 2705

【OpenClaw从入门到精通】第12篇:OpenClaw 2026子代理实战全攻略:用ACP架构搭建多智能体协作网络(附完整代码)

本文聚焦OpenClaw 2026.3.2版本默认启用的ACP(Agent Communication Protocol)子代理机制,从核心概念、底层原理到企业级实操全流程拆解。先解析子代理的定义、ACP协议通信原理及主从代理分工逻辑,再提供环境准备的详细步骤(版本验证、配置调整、技能安装);随后通过三个递进式虚拟案例(客服分流、智能文档处理、企业级招聘全流程),提供可直接运行的完整代码、执行结果及技术要点解析;最后补充子代理管理运维、性能优化与避坑指南。

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 2385

【并查集】 带权并查集详解

一.引入 并查集实际上是一颗由若干个树构成的森林,而如果还需要储存边的权值,则需要额外用一个数组d记录每个结点和其父节点的权值 这个思路其实很简单,但是因为路径压缩会改变点与点的连接关系,路径压缩后,点的父节点变为了根节点,所以需要更新d数组 二.查询函数写法 更新权值在查询操作的同时进行 int root_find(int x){ if(x==pre[x]) return x; int root=root_find(pre[x]); d[x]+=d[pre[...

Qiuker_jl的博客 2489

国科大学习资料--图像处理(彭思龙、杨戈)--2020年期末考试题解析(学长整理分析)

在对20年卷子分析前,先看看19年卷子的解析(后面有20年的变化,由于从核心课调整到了普及课,20年的卷子相对简单,但是还是有很多细节) 解析:(字写的不好别嫌弃!!!!) 20年的卷子 第一题:和19年基本一致,但是加了高斯核计算费用,要算一个节省计算成本比率,所以去多看看PPT 第二题:和19年基本一致,加了证明两个高斯卷积仍然是高斯卷积,相当于19年题2和3结合 第三题:采样定理,和19年基本一致,但要注意加了证明 第四题:考了形态学提取图像边界 第五题:直方图,和19年一

无名何许人的博客 4364

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

详解并查集

一、概念并差集主要用来求解不相交集合的问题,主要针对于合并和查找两种算法。二、并差集实现并差集的实现主要包含了三个部分: 初始化并差集。 合并并差集。 查找并差集。 (1)并差集的初始化并差集的初始化是对单个数据建立了一个单独的集合,每个集合应该包含以下两个数据: 这个集合所表示的数据。 这个集合的根集合所在位置。 由以上两个数据,一般的并差集的结构一般有两种表示形式: 结构体构造。 数组构造。

ArchyLi 1103

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

上一篇: Leetcode684. 冗余连接
下一篇: FFmpeg 命令:从入门到精通 | FFmpeg 基本介绍
UestcXiye
博客等级 码龄8年 9032粉丝 1306原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值