其它高阶数据结构①_并查集(概念+代码+两道OJ)

news2024/11/18 9:30:59

目录

1. 并查集的概念

2. 并查集的实现

3. 并查集的应用

3.1 力扣LCR 116. 省份数量

解析代码1

解析代码2

3.2 力扣990. 等式方程的可满足性

解析代码

本篇完。


写在前面:

        此高阶数据结构系列,虽然放在⑤数据结构与算法专栏,但还是作为一个拓展学习,建议跳过第⑤序号跟着其它专栏序号学,当时是想着要期末考和考研的同学,考到图才开这个专栏的吧,其他不急的同学可以在学完MySQL专栏后再看,此系列也放在了⑩其它高阶数据结构专栏,这里简单学习并查集是为了下一个数据结构“图”的学习。


1. 并查集的概念

  • 并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
  • 并查集通常用森林来表示,森林中的每棵树表示一个集合,树中的结点对应一个元素。

        虽然利用其它数据结构也能完成不相交集合的合并及查询,但在数据量极大的情况下,其耗费的时间和空间也是极大的。

        在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一 个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find set)。

        并查集是多个独立集合的合集,用于表示数据之间的关系,并查集中的每一个集合是用多叉树来表示的。

        比如:某公司今年校招全国总共招生10人,西安招4人,成都招3人,武汉招3人,10个人来自不 同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个 数。数组中某个位置的值为负数,表示该位置是树的根,这个负数的绝对值表示的这棵树(集合)中数据的个数,因为刚开始每个人各自属于一个集合,所以将数组中的位置都初始化为-1。

        毕业后,学生们要去公司上班,每个地方的学生自发组织成小分队一起上路,于是:西安学生小分队s1={0,6,7,8},成都学生小分队s2={1,4,9},武汉学生小分队s3={2,3,5}就相互认识了,10个人形成了三个小团体。假设右三个群主0,1,2担任队长,负责大家的出行。

一趟火车之旅后,每个小分队成员就互相熟悉,称为了一个朋友圈。

        从上图可以看出:编号6,7,8同学属于0号小分队,该小分队中有4人(包含队长0);编号为4和9的同 学属于1号小分队,该小分队有3人(包含队长1),编号为3和5的同学属于2号小分队,该小分队有3 个人(包含队长1)。 仔细观察数组中内变化,可以得出以下结论:

  1. 数组的下标对应集合中元素的编号。
  2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数。
  3. 数组中如果为非负数,代表该元素双亲在数组中的下标。

        在公司工作一段时间后,西安小分队中8号同学与成都小分队1号同学奇迹般的走到了一起,两个小圈子的学生相互介绍,最后成为了一个小圈子:

        现在0集合有7个人,2集合有3个人,总共两个朋友圈。通过以上例子可知,并查集一般可以解决一下问题:

  1. 查找元素属于哪个集合:沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)。
  2. 查看两个元素是否属于同一个集合:沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在。
  3. 将两个集合归并成一个集合:将两个集合中的元素合并,将一个集合名称改成另一个集合的名称。
  4. 集合的个数:遍历数组,数组中元素为负数的个数即为集合的个数。

2. 并查集的实现

代码实现还是很简单的,直接放出代码:(建议复制到自己编译器跟着注释一起看)

#pragma once

#include <iostream>
#include <vector>
using namespace std;

class UnionFindSet
{
private:
    vector<int> _ufs;

public:
    UnionFindSet(size_t size) // 初始时,将数组中元素全部设置为1
        : _ufs(size, -1)
    {}

    int FindRoot(int index) // 给一个元素的编号,找到该元素所在集合的名称
    {
        int root = index;
        while (_ufs[root] >= 0)   // 如果数组中存储的是负数,找到,否则一直继续
        {
            root = _ufs[root];
        }

        while (_ufs[index] >= 0) // 路径压缩
        {
            int parent = _ufs[index];
            _ufs[index] = root;

            index = parent;
        }
        return index;
    }

    bool InSet(int x1, int x2)
    {
        return FindRoot(x1) == FindRoot(x2);
    }

    bool Union(int x1, int x2) // 合并两个集合
    {
        int root1 = FindRoot(x1);
        int root2 = FindRoot(x2);

        if (root1 == root2) // x1已经与x2在同一个集合
            return false;

        if (abs(_ufs[root1]) < abs(_ufs[root2])) // 控制数据量小的往大的集合合并
            swap(root1, root2);

        _ufs[root1] += _ufs[root2]; // 负号代表根,数字代表该集合中元素个数

        _ufs[root2] = root1; // 将其中一个集合名称改变成另外一个
        return true;
    }

    size_t Count() const  // 数组中负数的个数,即为集合的个数
    {
        size_t count = 0;
        for (auto e : _ufs)
        {
            if (e < 0)
                ++count;
        }
        return count;
    }
};

void TestUFS()
{
    UnionFindSet u(10);

    u.Union(0, 6);
    u.Union(7, 6);
    u.Union(7, 8);

    u.Union(1, 4);
    u.Union(4, 9);

    u.Union(2, 3);
    u.Union(2, 5);

    u.FindRoot(6);
    u.FindRoot(9);

    cout << u.Count() << endl;
}


3. 并查集的应用

直接复制上面并查集的代码到力扣写两道题:

3.1 力扣LCR 116. 省份数量

LCR 116. 省份数量

难度 中等

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

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

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

返回矩阵中 省份 的数量。

示例 1:

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

示例 2:

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

提示:

  • 1 <= n <= 200
  • n == isConnected.length
  • n == isConnected[i].length
  • isConnected[i][j] 为 1 或 0
  • isConnected[i][i] == 1
  • isConnected[i][j] == isConnected[j][i]

注意:本题与主站 547 题相同: 547. 省份数量

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {

    }
};

解析代码1

直接复制并查集过来:

class UnionFindSet
{
private:
    vector<int> _ufs;

public:
    UnionFindSet(size_t size) // 初始时,将数组中元素全部设置为1
        : _ufs(size, -1)
    {}

    int FindRoot(int index) // 给一个元素的编号,找到该元素所在集合的名称
    {
        int root = index;
        while (_ufs[root] >= 0)   // 如果数组中存储的是负数,找到,否则一直继续
        {
            root = _ufs[root];
        }

        while (_ufs[index] >= 0) // 路径压缩
        {
            int parent = _ufs[index];
            _ufs[index] = root;

            index = parent;
        }
        return index;
    }

    bool InSet(int x1, int x2)
    {
        return FindRoot(x1) == FindRoot(x2);
    }

    bool Union(int x1, int x2) // 合并两个集合
    {
        int root1 = FindRoot(x1);
        int root2 = FindRoot(x2);

        if (root1 == root2) // x1已经与x2在同一个集合
            return false;

        if (abs(_ufs[root1]) < abs(_ufs[root2])) // 控制数据量小的往大的集合合并
            swap(root1, root2);

        _ufs[root1] += _ufs[root2]; // 负号代表根,数字代表该集合中元素个数

        _ufs[root2] = root1; // 将其中一个集合名称改变成另外一个
        return true;
    }

    size_t Count() const  // 数组中负数的个数,即为集合的个数
    {
        size_t count = 0;
        for (auto e : _ufs)
        {
            if (e < 0)
                ++count;
        }
        return count;
    }
};

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        UnionFindSet ufs(isConnected.size());
        for (size_t i = 0; i < isConnected.size(); ++i)
        {
            for (size_t j = 0; j < isConnected[i].size(); ++j)
            {
                if (isConnected[i][j] == 1) // 合并集合
                {
                    ufs.Union(i, j);
                }
            }
        }
        return ufs.Count();
    }
};


解析代码2

用数组模拟并查集:

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        vector<int> ufs(isConnected.size(), -1); // 手动控制并查集
        auto findRoot = [&ufs](int x) // 查找根
        {
            while(ufs[x] >= 0)
                x = ufs[x];
            return x;
        };

        for(size_t i = 0; i < isConnected.size(); ++i)
        {
            for(size_t j = 0; j < isConnected[i].size(); ++j)
            {
                if(isConnected[i][j] == 1) // 合并集合
                {
                    int root1 = findRoot(i);
                    int root2 = findRoot(j);
                    if (root1 != root2)
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root2] = root1;
                    }
                }
            }
        }
        int cnt = 0;
        for(auto e : ufs)
        {
            if(e < 0)
                ++cnt;
        }
        return cnt;
    }
};


3.2 力扣990. 等式方程的可满足性

990. 等式方程的可满足性

难度 中等

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。 

示例 1:

输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。

示例 2:

输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。

示例 3:

输入:["a==b","b==c","a==c"]
输出:true

示例 4:

输入:["a==b","b!=c","c==a"]
输出:false

示例 5:

输入:["c==c","b==d","x!=z"]
输出:true

提示:

  1. 1 <= equations.length <= 500
  2. equations[i].length == 4
  3. equations[i][0] 和 equations[i][3] 是小写字母
  4. equations[i][1] 要么是 '=',要么是 '!'
  5. equations[i][2] 是 '='
class Solution {
public:
    bool equationsPossible(vector<string>& equations) {

    }
};

解析代码

并查集的变形,思路:

  1. 将所有"=="两端的字符合并到一个集合中。
  2. 检测"!=" 两端的字符是否在同一个集合中,如果在,不满足,如果不在,满足。
class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        vector<int> ufs(26, -1);
        auto findRoot = [&ufs](int x)
        {
            while(ufs[x] >= 0)
                x = ufs[x];
            return x;
        };

        for(auto& e : equations) // 第一遍,先把相等的值加到一个集合中
        {
            if(e[1] == '=')
            {
                int root1 = findRoot(e[0] - 'a');
                int root2 = findRoot(e[3] - 'a');
                if(root1 != root2)
                {
                    ufs[root1] += ufs[root2];
                    ufs[root2] = root1;
                }
            }
        }

        for(auto& e : equations) // 第二遍,判断相等在不在一个集合,在就相悖了
        {
            if(e[1] == '!')
            {
                int root1 = findRoot(e[0] - 'a');
                int root2 = findRoot(e[3] - 'a');
                if(root1 == root2)
                    return false;
            }
        }
        return true;
    }
};


本篇完。

这里简单学习并查集更多是为了下一个数据结构“图”的学习,一些竞赛的OJ也会用到并查集。

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

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

相关文章

Output directory is not specified

场景&#xff1a;从GitHub拉取Java项目使用IDEA打开运行的时候抛出 java: 写入com.common.exception.ChatException时出错: Output directory is not specified网上大部分是说在项目结构增加编译器输出路径&#xff0c;但我在实际开发的项目的时候这里为空&#xff0c;包括我加…

IDEA找不到database图标的解决方法

首先右边侧边栏和左边的侧边栏都看一下&#xff0c;确认没有数据库图标以后再参考下面方法。 第一步&#xff0c;打开设置&#xff0c;在插件里搜索database 第二步 安装好&#xff0c;点击确定 返回主页面&#xff0c;左边的侧边栏会出现database图标&#xff0c;点击号就可以…

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

基础知识要求&#xff1a; Java&#xff1a;方法、while循环、for循环 Python&#xff1a; 方法、while循环、for循环 题目&#xff1a; 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head […

【C/C++笔试练习】DNS劫持、三次握手、TCP协议、HTTPS、四次挥手、HTTP报文、拥塞窗口、POP3协议、UDP协议、收件人列表、养兔子

文章目录 C/C笔试练习选择部分&#xff08;1&#xff09;DNS劫持&#xff08;2&#xff09;三次握手&#xff08;3&#xff09;TCP协议&#xff08;4&#xff09;HTTPS&#xff08;5&#xff09;四次挥手&#xff08;6&#xff09;HTTP报文&#xff08;7&#xff09;拥塞窗口&a…

宿舍管理系统代码详解(主页面)

本篇将对管理系统的主页面的代码进行详细的介绍。 目录 一、主页面前端代码 1.样式展示 2.代码详解 &#xff08;1&#xff09;template部分 &#xff08;2&#xff09;script部分 &#xff08;3&#xff09;路由导航守卫 &#xff08;4&#xff09;在vue中引用vue 一、主页…

富唯智能复合机器人:CNC铝块上下料安全新标准

在CNC铝块加工过程中&#xff0c;上下料环节的安全问题一直是企业关注的焦点。富唯智能复合机器人的应用&#xff0c;为这一环节树立了新的安全标准。 传统的上下料方式往往依赖于人工操作&#xff0c;存在着较大的安全隐患。而富唯智能复合机器人采用先进的视觉识别技术和精准…

Charger之二输入电压动态电源原理(VIN-DPM)

主要内容 Charger的VIN-DPM 前篇内容&#xff1a;电池管理IC&#xff08;Charger&#xff09;了解一下&#xff1f; 领资料&#xff1a;点下方↓名片关注回复&#xff1a;粉丝群 正文 一、 VIN-DPM概念 VIN-DPM是指输入电压动态电源管理&#xff08;Input voltage dynamic…

全栈开发之路——前端篇(9)插槽、常用api和全局api

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 第四篇&#xff1a;数据绑定、计算属性和watch监视 第五篇 : 组件…

OpenAI 把超强AI带进日常,GPT-4o 让机器也懂情感!

一、前言 ⭐⭐ 立即体验&#xff1a;GPT-4o OpenAI 在春季发布会上推出了名为 GPT-4o 的旗舰级生成式人工智能模型&#xff0c;这一模型的发布不仅标志着技术的巨大飞跃&#xff0c;更预示着人机交互方式的全面革新。"o" 在 GPT-4o 中代表 "omni"&#xf…

Codeforces Round 944 (Div. 4)(A,B,C,D,E,F,G,H)

比赛链接 这场不难&#xff0c; G G G 和 H H H 比较有意思。 G G G 题需要一定的二进制和数据结构的知识&#xff0c; H H H 题是个 2 − s a t 2-sat 2−sat 的题&#xff0c;算法名字吓人但是其实很简单&#xff0c;题目本身也很板&#xff0c;建议趁机学习一波。 A. My …

vue获取路由的值

1&#xff0c;此方法获取到请求地址后面的值 如 /name123&age12 2&#xff0c;此方法获取到请地址&#xff1f;后面的值 例如?name123&age12 二者的区别&#xff0c;第一个是直接在路径后面拼接&#xff0c;第二种就是正规的http请求。 路径带&#xff1f;号的

Ajax 学习

文章目录 1. 前置知识1.1 ajax 介绍1.2 XML 简介 2. AJAX 学习2.1 AJAX基础学习&#xff08;1&#xff09;AJAX的特点&#xff08;2&#xff09;AJAX 初体验&#xff08;3&#xff09;服务端响应json 数据 2.2 IE 缓存问题2.3 请求超时和网络异常2.4 手动取消请求2.5 重复请求2…

使用DBeaver的第2天-使用sql导入数据

使用sql导入数据这块我会仔细的说一下 首先位置一定要放在库上&#xff08;实例&#xff09;&#xff0c;放在表上可不好使用哦 然后点击工具-再点击执行脚本 这样就执行成功了 但是如果你执行失败了&#xff0c;多半可能是因为本地没有部署mysql&#xff0c;记住只有本地有…

搜索引擎的设计与实现(四)

目录 6 系统测试 6.1测试重要性 6.2测试用例 结 论 参 考 文 献 前面内容请移步 搜索引擎的设计与实现&#xff08;三&#xff09; 免费源代码&毕业设计论文 搜索引擎的设计与实现 6 系统测试 6.1测试重要性 该项目是在本地服务器上进行运行和调试&#xff0c;…

BOM部分

一&#xff0c;概述 二&#xff0c;Windows对象常见的事件 1.窗口加载事件 上面那个是会等页面都加载完了&#xff0c;在进行函数的调用或者触发事件&#xff0c;如&#xff08;图像&#xff0c;文本&#xff0c;css&#xff0c;js等&#xff09;&#xff0c;所以那个声明可以…

IT行业找工作十面十败,不妨试试鸿蒙开发岗~

近期某脉上看到这样一则帖子&#xff0c;讨论的非常激烈&#xff01; 相信也有不少人有和他这情况类似&#xff0c;像他这种失业的状态&#xff0c;近两年大家或多或少都深有体验。由于互联网行业进过了十几年的快速发展&#xff0c;从2G→3G→4G→5G&#xff0c;在这个期间人们…

Linux基础之进程-进程状态

目录 一、进程状态 1.1 什么是进程状态 1.2 运行状态 1.2 阻塞状态 1.3 挂起状态 二、Linux操作系统上具体的进程状态 2.1 状态 2.2 R 和 S 状态的查看 2.3 后台进程和前台进程 2.4 休眠状态和深度休眠状态 一、进程状态 1.1 什么是进程状态 首先我们知道我们的操作系…

未来互联网:Web3的技术革新之路

引言 随着技术的不断发展和社会的日益数字化&#xff0c;互联网作为信息交流和社交媒介的重要平台已经成为我们生活中不可或缺的一部分。然而&#xff0c;传统的互联网架构在数据安全、隐私保护和去中心化等方面存在着诸多挑战。为了解决这些问题&#xff0c;Web3技术应运而生…

【js】获取媒体流实现拍照功能,摄像头切换

<script setup>import {onMounted,reactive,ref} from vueconst videoConstraints reactive({width: 500,height: 300});let picArr reactive([])let videoNode ref(null)let show ref(true)let stream reactive({})onMounted(async () > {// 获取视频流&#xf…

C++语法|深入理解 new 、delete

在开发过程中&#xff0c;非常重要的语法就有我们new和delete&#xff0c;周所周知在C中最为强大的能力就是对内存的控制&#xff0c;所以我们再怎么强调new和delete都不为过 文章目录 1.new和delete基本语法new和malloc的区别是什么&#xff1f;(1)开辟单个元素的内存差别(2)开…