并查集(高阶数据结构)

目录

一、并查集的原理

二、并查集的实现

2.1 并查集的初始化

2.2 查找元素所在的集合

2.3 判断两个元素是否在同一个集合

2.4 合并两个元素所在的集合

2.5 获取并查集中集合的个数

2.6 并查集的路径压缩

2.7 元素的编号问题

三、并查集题目

3.1 省份的数量

3.2 等式方程的可满足性


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

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

一、并查集的原理

以朋友圈为例,现在有10个人(从0开始编号),刚开始这10个人互不认识,各自属于一个集合

并查集会用一个数组来表示这10个人之间的关系,数组的下标对应就是这10个人的编号,刚开始时数组中的元素都初始化为-1

说明:数组中某个位置的值为负数,表示该位置是树的根,这个负数的绝对值表示的这棵树(集合)中数据的个数,因为刚开始每个人各自属于一个集合,所以将数组中的位置都初始化为-1

后来这10个人之间通过相互认识,最终形成了三个朋友圈

此时并查集数组中各个位置的值如下

说明:数组中某个位置的值为非负数,表示该位置不是树的根,这个非负数的值就是这个结点的父结点的编号

后来4号和8号又通过某种机遇互相认识了,这时其所在的两个集合就需进行合并,最终就变成了两个朋友圈

在根据两个元素合并两个集合时,需先分别找到这两个元素所在集合的根结点,然后再将一个集合合并到另一个集合,并且合并后需要更新数组中根结点的值

合并集合找根结点的原因:

  1. 若这两个元素所在集合的根结点相同,说明这两个元素本身就在同一个集合,无需合并
  2. 合并集合后需要更新这两个集合的根结点的值

二、并查集的实现

实现并查集时通常会实现如下接口:

  • 初始化并查集
  • 查找元素所在的集合
  • 判断两个元素是否在同一个集合
  • 合并两个元素所在的集合
  • 获取并查集中集合的个数
#include <iostream>
#include <vector>
using namespace std;

class UnionFindSet
{
public:
	// 构造函数
	UnionFindSet(size_t size);
	// 查找元素所在的集合
	int FindRoot(int value);
	// 判断两个元素是否在同一个集合
	bool IsSameSet(int value1, int value2);
	// 合并两个元素所在的集合
	bool Union(int value1, int value2);
	// 获取并查集中集合的个数
	size_t GetSetSize();
private:
	vector<int> _ufs; //维护各个结点间的关系
};

并查集中的数组:

  • 数组的下标依次对应每个元素的编号
  • 数组中元素值为负数,表示下标编号元素为根结点,负数的绝对值表示该集合中元素的个数
  • 数组中元素值为非负数,表示下标编号元素的父结点的编号

2.1 并查集的初始化

并查集中会用一个数组来维护各个结点之间的关系,在初始化并查集时,根据元素的个数开辟数组空间,并将数组中的元素初始化为-1即可

UnionFindSet(size_t size): _ufs(size, -1) {}

2.2 查找元素所在的集合

查找元素所在的集合,本质就是查找元素所在集合的根结点

查找逻辑如下:

  • 若元素对应下标位置存储的是负数,则说明该元素即为根结点,返回该元素即可
  • 若元素对应下标位置存储的是非负数,则跳转到其父结点的位置继续查找根结点

迭代方式实现:

int FindRoot(int value)
{
	int root = value;
	while (_ufs[root] >= 0) 
        root = _ufs[root];
	return root;
}

递归方式实现:

int FindRoot(int x) {
	return _ufs[x] < 0 ? x : FindRoot(_ufs[x]);
}

2.3 判断两个元素是否在同一个集合

要判断两个元素是否在同一个集合,本质就是判断这两个元素所在集合的根结点是否相同

bool IsSameSet(int value1, int value2) {
	return FindRoot(value1) == FindRoot(value2);
}

2.4 合并两个元素所在的集合

合并逻辑如下:

  1. 分别找到两个元素所在集合的根结点
  2. 若这两个元素所在集合的根结点相同,则无需合并。若这两个元素所在集合的根结点不同,则将小集合合并到大集合上
  3. 将小集合根结点的值累加到大集合的根结点上,使得大集合根结点的值的绝对值等于两个集合中元素的总数。
  4. 将小集合根结点的值改为大集合根结点的编号,即将小集合的根结点作为大集合根结点的孩子,使得两个集合变为一个集合
bool Union(int value1, int value2)
{
	int root1 = FindRoot(value1), root2 = FindRoot(value2);
	//本身在同一个集合,不需合并
	if (root1 == root2) return false;
	//合并操作: 数据量小的 往 数据量大的 合并
	if (abs(_ufs[root1]) < abs(_ufs[root2])) swap(root1, root2);
	_ufs[root1] += _ufs[root2];
	_ufs[root2] = root1;
	return true;
}

说明:当两个集合合并时,尽量将小集合合并到大集合上,因为被合并的那个集合中的所有结点在合并后层数都会加一,所以这样做的目的就是为了让较少的结点层数加一,该操作不是必须的

2.5 获取并查集中集合的个数

获取并查集中集合的个数,本质就是统计数组中负值(根结点)的个数

size_t GetSetSize()
{
	size_t count = 0;
	for (int i = 0; i < _ufs.size(); ++i)
		if (_ufs[i] < 0) ++count;
	return count;
}

2.6 并查集的路径压缩

当数据量很大的时候,并查集中树的层数可能会变得很高,这时查找一个元素所在集合的根结点时就需要往上走很多层,此时可以考虑进行路径压缩

路径压缩一般会在查找根结点时进行,当根据一个结点查找其根结点时,该路径上所有的结点都会被压缩,最终这些结点会直接被挂在根结点下,下次再根据这些结点查找根结点时就能快速找到根结点

迭代方式实现:

int FindRoot(int value)
{
	int root = value;
	while (_ufs[root] >= 0) root = _ufs[root];
	//路径压缩
	while (_ufs[value] >= 0)
	{
		int parent = _ufs[value];
		_ufs[value] = root;
		value = parent;
	}
	return root;
}

递归方式实现:

int FindRoot(int x) 
{
	int parent = x; //默认当前结点就是根结点
	if (_ufs[x] >= 0) { //当前结点值不是负数则继续向上找
		parent = FindRoot(_ufs[x]); //找到根结点
		_ufs[x] = parent; //将当前结点的父亲改为根结点(路径压缩)
	}
	return parent;
}

2.7 元素的编号问题

上面在实现并查集时,默认元素的编号都是从0开始依次递增的,但用户所给的编号可能并不是从0开始的,也不是连续的,甚至可能不是数字

可以用模板的方式来实现并查集:

  • 在初始化并查集时,根据所给元素建立元素与数组下标之间的映射关系。
  • 在查找元素所在集合的根结点时,先根据所给元素得到其对应的数组下标,然后再进行查找
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

template<class T>
class UnionFindSet
{
public:
	// 构造函数
	UnionFindSet(const vector<T>& v): _ufs(v.size(), -1) 
	{
		for (int i = 0; i < v.size(); ++i)
			_indexMap[v[i]] = i;
	}
	
	// 查找元素所在的集合
	int FindRoot(const T& value)
	{
		int root = _indexMap[value];
		while (_ufs[root] >= 0) root = _ufs[root];
		//路径压缩
		int tmp = _indexMap[value];
		while (_ufs[tmp] >= 0) {
			int parent = _ufs[tmp];
			_ufs[tmp] = root;
			tmp = parent;
		}
		return root;
	}

	// 判断两个元素是否在同一个集合
	bool IsSameSet(const T& value1, const T& value2) {
		return FindRoot(value1) == FindRoot(value2);
	}

	// 合并两个元素所在的集合
	bool Union(const T& value1, const T& value2)
	{
		int root1 = FindRoot(value1), root2 = FindRoot(value2);
		//本身在同一个集合,不需合并
		if (root1 == root2) return false;
		//合并操作: 数据量小的 往 数据量大的 合并
		if (abs(_ufs[root1]) < abs(_ufs[root2])) swap(root1, root2);
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
		return true;
	}

	// 获取并查集中集合的个数
	size_t GetSetSize()
	{
		size_t count = 0;
		for (int i = 0; i < _ufs.size(); ++i)
			if (_ufs[i] < 0) ++count;
		return count;
	}
private:
	vector<int> _ufs; //维护各个结点间的关系
	unordered_map<T, int> _indexMap;//维护元素与下标之间的映射关系
};

再使用并查集时就可以传入任意类型的元素了

int main() 
{
	vector<string> v = { "张三", "李四", "王五", "赵六", "田七", "周八", "吴九" };

	UnionFindSet<string> ufs(v);
	cout << ufs.GetSetSize() << endl; //7

	ufs.Union("张三", "李四");
	ufs.Union("王五", "赵六");
	cout << ufs.GetSetSize() << endl; //5

	ufs.Union("张三", "赵六");
	cout << ufs.GetSetSize() << endl; //4

	return 0;
}

三、并查集题目

3.1 省份的数量

LCR 116. 省份数量 - 力扣(LeetCode)

class Solution 
{
public:
    int findCircleNum(vector<vector<int>>& isConnected) 
    {
        vector<int> ufs(isConnected.size(), -1);
        auto findRoot = [&ufs](int value) {
            while(ufs[value] >= 0) value = ufs[value];
            return value;
        };
        auto getSetSize = [&ufs]() {
            size_t count = 0;
            for(int i = 0; i < ufs.size(); ++i)
                if(ufs[i] < 0) ++count;
            return count;
        };
        for(size_t i = 0; i < isConnected.size(); ++i)
            for(size_t j = 0; j < isConnected[0].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;
                    }
                }
        return getSetSize();
    }
};

3.2 等式方程的可满足性

990. 等式方程的可满足性 - 力扣(LeetCode)

class Solution {
public:
    bool equationsPossible(vector<string>& equations) 
    {
        vector<int> ufs(26, -1);
        auto findRoot = [&ufs](int value) {
            while(ufs[value] >= 0) value = ufs[value];
            return value;
        };
        //第一遍,先把相等的值合并到一个集合中
        for(auto& str : equations)
        {
            if(str[1] == '=')
            {
                int root1 = findRoot(str[0] - 'a');
                int root2 = findRoot(str[3] - 'a');
                if(root1 != root2)
                {
                    ufs[root1] += ufs[root2];
                    ufs[root2] = root1;
                }
            }
        }
        //第二遍,查看不相等的值在不在一个集合,在就相悖,返回false
        for(auto& str : equations)
        {
            if(str[1] == '!')
            {
                int root1 = findRoot(str[0] - 'a');
                int root2 = findRoot(str[3] - 'a');
                if(root1 == root2) return false;
            }
        }
        return true;
    }
};

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

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

相关文章

【大厂AI课学习笔记】1.4 算法的进步(1)

2006年以来&#xff0c;以深度学习为代表的机器学习算法的发展&#xff0c;启发了人工智能的发展。 MORE&#xff1a; 自2006年以来&#xff0c;深度学习成为了机器学习领域的一个重要分支&#xff0c;引领了人工智能的飞速发展。作为人工智能专家&#xff0c;我将阐述这一时期…

[蓝桥杯难题总结-Python]乘积最大

乘积最大 题目描述 今年是国际数学联盟确定的“ 2000 ——世界数学年”&#xff0c;又恰逢我国著名数学家华罗庚先生诞辰 90 周年。在华罗庚先生的家乡江苏金坛&#xff0c;组织了一场别开生面的数学智力竞赛的活动&#xff0c;你的一个好朋友 XZ 也有幸得以参加。活动中&…

xss 盲打使用

使用beef等内网xss平台&#xff0c;或外网xss平台&#xff08;XSS平台-仅用于xss安全测试专用、XSS平台 - &#xff08;支持http/https&#xff09;XSS Platform&#xff09; 将生成的js脚本写到网站的留言框处&#xff0c;但对应的用户(尤其是admin)查看留言&#xff0c;就会…

Vue学习之使用HBuilderX创建并使用vue3.0项目

Vue学习之使用HBuilderX创建并使用vue3.0项目 下文将简述如何使用HBuilderX创建并使用vue3.0项目&#xff0c;包含项目创建、目录介绍、如何引用组件、首页自定义设置。 1、创建vue3.0项目 具体操作之前章节已经阐述过不在冗余介绍&#xff0c;创建时选择vue3项目即可。vue2…

探索自然语言处理在改善搜索引擎、语音助手和机器翻译中的应用

文章目录 每日一句正能量前言文本分析语音识别机器翻译语义分析自然语言生成情感分析后记 每日一句正能量 努力学习&#xff0c;勤奋工作&#xff0c;让青春更加光彩。 前言 自然语言处理&#xff08;NLP&#xff09;是人工智能领域中与人类语言相关的重要研究方向&#xff0c…

C++并发编程 -2.线程间共享数据

本章就以在C中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时&#xff0c;将共享数据的优势发挥到最大。 一. 锁分类和使用 按照用途分为互斥、递归、读写、自旋、条件变量。本章节着重介绍前四种&#xff0c;条件变量后续章节单独介绍。 由于锁无法进行拷贝…

Python之PySpark简单应用

文章目录 一、介绍1.准备工作2. 创建SparkSession对象&#xff1a;3. 读取数据&#xff1a;4. 数据处理与分析&#xff1a;5. 停止SparkSession&#xff1a; 二、示例1.读取解析csv数据2.解析计算序列数据map\flatmap 三、问题总结1.代码问题2.配置问题 一、介绍 PySpark是Apa…

云计算基础(云计算概述)

目录 一、云计算概述 1.1 云计算的概念 1.1.1 云计算解决的问题 1.1.2 云计算的概念 1.1.3 云计算的组成 1.2 云计算主要特征 1.2.1 按需自助服务 1.2.2 泛在接入 1.2.3 资源池化 1.2.4 快速伸缩性 1.2.5 服务可度量 1.3 云计算服务模式 1.3.1 软件即服务(Softwar…

3D词云图

工具库 tagcanvas.min.js vue3&#xff08;框架其实无所谓&#xff0c;都可以&#xff09; 实现 <script setup> import { onMounted, ref } from vue; import ./tagcanvas.min.js;const updateFlag ref(false);// 词云图初始化 const initWordCloud () > {let …

【echarts】动态滚动趋势图,解决坐标轴数据太多遮挡覆盖问题

写在前面 业务场景x轴的文字太多&#xff0c;会出现遮挡问题&#xff0c;想到文字倾斜展示&#xff0c;页面不美观&#xff0c;于是想到使用滚动条优化趋势图。 <template><div id"storeDown" style"height: 400px;width:100%"/> </temp…

LEETCODE 75. 颜色分类

class Solution { public:void sortColors(vector<int>& nums) {//先定0int i,j;i0;j0;int nnums.size();while(j<n){if(nums[j]0){int tmpnums[j];nums[j]nums[i];nums[i]tmp;j1;i1;}else{j1;}}//对[i,n]处理&#xff0c;定1int i1i;ji1;while(j<n){if(nums[j…

小程序支付类型接入京东支付

一、情景描述 当前项目想在微信小程序付款时添加上京东支付支付类型&#xff0c;效果如下 普通的付款方式可以直接付款就能完成支付&#xff0c;但京东支付无法在小程序上直接付款&#xff0c;他需要复制生成的链接&#xff0c;然后打开京东app然后在京东平台上付款。 所以&…

Vue(二十):ElementUI 扩展实现表格组件的拖拽行

效果 源码 注意&#xff1a; 表格组件必须添加 row-key 属性&#xff0c;用来优化表格的渲染 <template><el-row :gutter"10"><el-col :span"12"><el-card class"card"><el-scrollbar><span>注意: 表格组件…

c++设计模式之观察者模式(发布-订阅模式)

介绍 观察者模式主要关注于对象的一对多关系&#xff0c;其中多个对象都依赖于一个对象&#xff0c;当该对象的状态发生改变时&#xff0c;其余对象都能接收到相应的通知。 如&#xff0c;现在有 一个数据对象三个画图对象&#xff0c;分别wield曲线图、柱状图、饼状图三个对象…

AI Prompt工程师 学习整理

前言 如果说Al大语言模型(LLM,Large Language Model)是宝藏我,那么Prompt提示词就是打开宝藏的钥匙。 最新一代的Al大语言模型具备出色的创作能力,能够生成富有人类感情、严谨逻辑、多场景应用的内容,而如何获得高质量的回答,正确学习使用Prompt提示词是关键。 &#x1f4a5…

详解WebRTC rtc::Thread实现

rtc::Thread介绍 rtc::Thread类不仅仅实现了线程这个执行器&#xff08;比如posix底层调用pthread相关接口创建线程&#xff0c;管理线程等&#xff09;&#xff0c;还包括消息队列&#xff08;message_queue)的实现&#xff0c;rtc::Thread启动后就作为一个永不停止的event l…

2023爱分析·知识库问答市场厂商评估报告:爱数

01 研究范围定义 研究范围&#xff1a; 大模型是指通过在海量数据上依托强大算力资源进行训练后能完成大量不同下游任务的模型。2023年以来&#xff0c;ChatGPT引爆全球大模型市场。国内众多大模型先后公测&#xff0c;众多互联网领军者投身大模型事业&#xff0c;使得大模型…

【Linux】环境基础开发工具的使用之gcc详解(二)

前言&#xff1a;上一篇文章中我们讲解了Linux下的vim和yum的工具的使用&#xff0c;今天我们将在上一次的基础上进一步的讲解开放工具的时候。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:Linux的深度刨析 &#x1f448; &#x1f4a…

贰[2],Xamarin生成APK

1&#xff0c;生成改为Release版本 2&#xff0c;选中****.Android项目 3&#xff0c;点击生成&#xff0c;选择存档 4&#xff0c;点击分发 5&#xff0c;选择临时 6&#xff0c;添加签名标识 7&#xff0c;选择对应的签名标识&#xff0c;点击另存为

文献阅读:金鱼端脑细胞类型图谱揭示了空间结构和细胞类型进化的多样性

文献介绍 「文献题目」 A telencephalon cell type atlas for goldfish reveals diversity in the evolution of spatial structure and cell types 「研究团队」 Amit Zeisel&#xff08;以色列理工学院&#xff09;、Ronen Segev&#xff08;本古里安大学&#xff09; 「发表…