并查集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的初始值为0
        for(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();
        //visit
        isVisited[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])
                {
                    //visit
                    isVisited[i] = true;
                    queue.push(i);
                }
            }
        }
    }
    
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Echo框架:高性能的Golang Web框架

Echo框架&#xff1a;高性能的Golang Web框架 在Golang的Web开发领域&#xff0c;选择一个适合的框架是构建高性能和可扩展应用程序的关键。Echo是一个备受推崇的Golang Web框架&#xff0c;以其简洁高效和强大功能而广受欢迎。本文将介绍Echo框架的基本特点、使用方式及其优势…

学习shell脚本

文章目录 什么是shell脚本为什么要学习shell脚本第一个脚本编写与执行 简单的shell脚本练习简单案例脚本的执行方式差异(source、sh script、./script) 如何使用shell脚本的判断式利用test命令的测试功能利用判断符号[ ]shell脚本的默认变量($0、$1...) shell脚本的条件判断式利…

MySQL安装(Mac系统)

首先要删除本机原有的mysql 停止MySQL服务 sudo /usr/local/mysql/support-files/mysql.server stop不放心可以使用以下命令查询并杀死进程 ps aux | grep mysqld sudo kill <PID>再次尝试停止服务 sudo /usr/local/mysql/support-files/mysql.server stop卸载MySQL&…

多租户平台前端存储结构的选择

下图来源于cookie、localStorage 和 sessionStorage的区别及应用实例 既然localstorage无有效期&#xff0c;关闭浏览器还存在&#xff0c;那么用来存储用户的身份信息并不是太合适&#xff0c;先看一下B站中localstorage都存在了啥&#xff0c;原来把我搜索的记录都存在了下来…

第十二届蓝桥杯EDA省赛真题分析

前言&#xff1a; 第十二届蓝桥杯EDA比赛用的是AD软件&#xff0c;从第十四届起都是使用嘉立创EDA专业版&#xff0c;所以在这里我用嘉立创EDA专业版实现题目要求。 一、省赛第一套真题题目 主观题整套题目如下&#xff1a; 试题一&#xff1a;库文件设计&#xff08;5分&am…

【类脑智能】脑网络通信模型分类及量化指标(附思维导图)

脑网络通信模型分类及量化指标(附思维导图) 参考论文&#xff1a;Brain network communication_ concepts, models and applications 概念 脑网络通信模型是一种使用图论和网络科学概念来描述和量化大脑结构中信息传递的模型。这种模型可以帮助研究人员理解神经信号在大脑内…

P1881 绳子对折

题目描述 FJ 有一个长度为 L&#xff08;1≤L≤10,000&#xff09;的绳子。这个绳子上有 N&#xff08;1≤N≤100&#xff09;个结&#xff0c;包括两个端点。FJ 想将绳子对折&#xff0c;并使较短一边的绳子上的结与较长一边绳子上的结完全重合&#xff0c;如图所示&#xff…

知名Web3投资基金a16z合伙人Jane Lippencott确认出席Hack.Summit() 2024区块链开发者大会

在区块链技术的风起云涌和Web3生态的蓬勃发展中&#xff0c;知名a16z Crypto的合伙人Jane Lippencott已确认出席即将于2024年4月9日至10日在香港数码港举行的Hack.Summit() 2024区块链开发者大会。作为亚洲首次举办的Hack.Summit()&#xff0c;此次大会将为全球区块链开发者及业…

本地用AIGC生成图像与视频

最近AI界最火的话题&#xff0c;当属Sora了。遗憾的是&#xff0c;Sora目前还没开源或提供模型下载&#xff0c;所以没法在本地跑起来。但是&#xff0c;业界有一些开源的图像与视频生成模型。虽然效果上还没那么惊艳&#xff0c;但还是值得我们体验与学习下的。 Stable Diffu…

深度学习-基于机器学习的情绪分析研究

概要 互联网技术的迅速发展使得社交平台逐渐成为热点事件中社会情感的枢纽。社会热点事件的舆论监管的其中一个重要环节就是能够准确分析民众的社会情绪。本文旨在探索可以基于文本大数据彻底分析民众对热点事件的社会情绪的模型和方法。先是从社交平台上借助文本大数据、对数据…

计算机网络 |内网穿透

其实内网穿透&#xff0c;也挺好玩的&#xff0c;如果在大学的时候&#xff0c;那个时候讲计算机网络的老师能横向延展&#xff0c;估计课也会更有趣不少&#xff0c;本来计算机网络这门课就是计算机课程中可玩性最搞的。 只能说&#xff0c;怪可惜的 回到正题&#xff0c;内网…

【Stable Diffusion】入门-04:不同模型分类+代表作品+常用下载网站+使用技巧

目录 1 模型简介2 模型文件构成和加载位置2.1 存储位置2.2 加载模型 3 模型下载渠道3.1 HuggingFace3.2 Civitai 4 模型分类4.1 二次元模型4.2 写实模型4.3 2.5D模型 1 模型简介 拿图片给模型训练的这个过程&#xff0c;通常被叫做“喂图”。模型学习的内容不仅包括对具体事物…

数据结构的概念大合集03(栈)

概念大合集03 1、栈1.1 栈的定义和特点1.2 栈的基础操作1.3 栈的顺序存储1.3.1 顺序栈1.3.2 栈空&#xff0c;栈满&#xff0c;进栈&#xff0c;出栈的基本思想1.3.3 共享栈1.3.3.1 共享栈的4要素 1.4 栈的链式存储1.4.1 链栈的实现1.4.2 链栈的4个要素 1、栈 1.1 栈的定义和特…

客户端:Vue3,服务端:Node,基于Socket.IO实现单聊的功能

目录 1.介绍 2.环境搭建 3.本功能实现的主要逻辑 4.客户端和服务端的主要代码 5.效果展示 6.socket.io的运作原理 1.介绍 本篇主要讲讲基于Socket.IO实现单聊功能的主要实现&#xff0c;包括了客户端和服务端Node。 在这个即时通讯无处不在的时代&#xff0c;实时聊天功能…

波奇学Linux:线程安全和自选锁和读写锁

STL不是线程安全的 单例模式的线程安全 自选锁&#xff1a;当线程申请锁失败时&#xff0c;不是挂起&#xff0c;而是一直申请 挂起等待锁 &#xff1a;当线程申请锁失败时&#xff0c;把锁挂起 一般临界区时间短的适合自选锁&#xff0c;长的适合挂起等待锁

如何在“Microsoft Visual Studio”中使用OpenCV编译应用程序

返回目录&#xff1a;OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 前一篇&#xff1a;OpenCV4.9.0在windows系统下的安装 后一篇&#xff1a; 警告&#xff1a; 本教程可以包含过时的信息。 我在这里描述的所有内容都将适用于 OpenCV 的C\C接口。我首先假…

wsl ubuntu 安装的正确方式

目录 wsl ubuntu 安装的正确方式&#xff1a; 将wsl2设置为默认版本&#xff1a; 1、打开powershell 2、设置wsl的版本为2 ​编辑 3、更新wsl程序 4、强制关闭子系统 5、查看wsl支持的列表 6、安装指定版本的系统 wsl ubuntu 安装的正确方式&#xff1a; 此时&#xff0c…

Leetcode31. 删除无效的括号

心路历程&#xff1a; 一开始看到有点懵&#xff0c;后来发现有点像按照一定规则穷举所有可能情况&#xff0c;想到了排列组合问题&#xff0c;再结合问题长度不固定&#xff0c;无法用已知个for循环表示&#xff0c;从而想到了回溯。这个题相当于需要在一定规则下枚举。 按照…

刚刚离乳的幼猫该如何选择猫粮品牌?

亲爱的猫友们&#xff0c;当你家的幼猫刚刚离乳&#xff0c;准备踏入猫粮的世界时&#xff0c;如何选择一款合适的猫粮品牌确实是个让人头疼的问题。&#x1f43e; 别担心&#xff0c;今天我就来为大家推荐一款值得信赖的幼猫粮——福派斯幼猫粮。 1️⃣ 考虑幼猫的营养需求 幼…

SQLiteC/C++接口详细介绍之sqlite3类(十三)

返回目录&#xff1a;SQLite—免费开源数据库系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;十二&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;十四&#xff09;&#xff08;未发表&#xff09; 40.sqlite3…