数据结构 | C++ | 并查集原理讲解与模拟实现 | 并查集的相关习题

news2024/11/27 10:26:01

文章目录

    • 前言
    • 并查集原理
    • 并查集的模拟实现
    • leetcode练习
      • 省份数量
      • 等式方程的可满足性

前言

并查集通常会作为高阶数据结构的一个子结构使用,虽然原理不是很难,但其思想值得我们好好学习

并查集原理

并查集是一种树形结构,其保存了多个集合,每个集合以树的形式体现,所以说并查集是一片森林。不像二叉树有着严格的节点限制,并查集的树可以有任意棵子树,这也导致了在使用并查集处理海量数据时需要设计路径压缩算法,以提高查找元素的效率。当然这是后话了,并查集用来做什么呢?其最主要的应用是判断两个元素是否在同一集合(同一棵树)中,由此衍生了其他应用:查找一个元素所在的集合,将两个不同集合的元素合并从而使两个集合合并为一个集合,以及并查集中的集合个数。

那么并查集通过什么表示元素之间的关系呢?和优先级队列(大小堆)相似,优先级队列用数组下标表示节点之间的关系,元素存储在数组中。并查集相反,元素被抽象成数组下标数组中存储的值表示元素间的关系,通常这个数组存储int,并初始化为-1,数组用整数来表示元素之间的关系。这里画图讲解
在这里插入图片描述
虽然并查集是树形结构,但是它与优先级队列一样,逻辑上是树形结构,物理上确是一个连续的数组。从上图中可以看出,数组有10个元素(这里为了讲解方便,假设元素的值与数组下标相同),每个元素在并查集中的值都是-1,这表示每个元素自成一个集合,互相之间没有联系。现在将3和4合并到一个集合,假设3是集合中的根,我们要做的是把4在并查集中的值加到3在并查集中的值上,修改为-2。接着将4在并查集中的值修改为3在并查集中的下标,由于3在并查集中的下标就是3,所以把4在并查集中的值修改为3在这里插入图片描述
这也就能解释为什么要把并查集中的值初始化为-1了,3和4现在处于一个集合中,3作为集合的根,它在并查集中的值是-2,绝对值为2表示这个集合中有2个元素,而4在并查集中的值为3,这个值就是3在并查集中的下标。所以总结一下,负数表示该元素是一个集合的根,其绝对值就是该集合中的元素个数。一开始元素之间互相没有联系,所以它们的值是-1,说明它们自成一个集合并作为集合的根。当一个元素在并查集中的值是一个正数,说明该元素处于一个集合中,且该元素在并查集中的值就是其双亲元素在并查集中的下标。4在并查集中的值是3,说明4处于一个集合中,其双亲节点在并查集中的下标是3

在这里插入图片描述
假设现在元素之间的关系如上图所示,那么并查集会是怎样的?在这里插入图片描述
现在并查集中有三个集合,它们的根分别是0,1,2,所以0,1,2在并查集中的值都是负数,它们的子元素在并查集中的值为它们在并查集中的下标。理解了并查集中元素之间的映射关系,现在就可以讲解并查集的算法了。第一个算法是合并两个元素,其本质是两个集合之间的合并,使两个集合的元素都联系起来,比如我要把8和4合并,由于8和4都是集合的子元素,所以我们要先找到集合的根元素,根据元素在并查集中的值为正数,说明它是集合中的子元素,并且该值是其双亲元素在并查集中的下标,所以我们可以在并查集中找到它的双亲元素,如果该元素在并查集中的值为负数,该元素就是集合的根元素。这就是查找一个元素所在集合的算法逻辑,根据这个逻辑找到两个子元素的根元素,再判断哪个集合的元素多,将元素少的集合合并到元素多的集合中。8所在的集合有4个元素,比4所在集合的元素个数多,所以将4所在集合合并到8所在集合中。此时我们只要把4所在集合的根元素,1作为8所在集合的根元素的子元素即可:把1在并查集中的值加到0在并查集中的值上,然后把1在并查集中的值修改为0在并查集中的下标——0在这里插入图片描述
这就是合并算法的逻辑,至于判断两个元素是否在同一集合中,只要分别查找两个元素的根元素,判断根元素是否相等即可,查找根元素的逻辑刚才也说过了。至此并查集的算法也大概讲解完了,接下来是其模拟实现

并查集的模拟实现

可以注意到,我在讲解算法时不厌其烦的提到某元素在并查集中的值,而不是直接说某元素的值呢?讲解算法之前我做了一个假设,元素的值与数组下标的值相等,就是说并查集存储的元素是一些整数。但并查集可不是一定只能存储整数的,它还可能存储字符串,一些自定义类型,那么这些类型与整数之间没有直接的关系,我们就要将它们抽象成整数。比如要判断两个字符串是否在一个集合中,我们要做的是先将它们转换成整数,将这个整数作为数组下标,根据该位置的值进行后续的判断。而刚才我的讲解没有映射的过程,或者说是直接映射,元素与数组下标的值相等,对此就不需要做转换。但是,针对泛型,我们需要保存其与整数之间的映射关系,可以使用unordered_map保存,将泛型T与int整数作为pair对象,存储进unordered_map中,使用者传入泛型对象,我们需要将其转换成int对象再进行相关的计算

template <class T>
class UnionFindSet
{
public:
	UnionFindSet(const T* arr, size_t n) // 用一个泛型数组初始化,并指定元素个数n
	{
		for (size_t i = 0; i < n; ++i)
		{
			add_elm(arr[i]);                 // 接口的复用
		}
	}

	// 添加元素到并查集中
	void add_elm(const T& data)
	{
		// typename unordered_map<T, int>::iterator ret = _to_int.find(arr[i]);
		auto ret = _to_int.find(data);

		if (ret == _to_int.end())       // 并查集中没有这个元素才可以存储
		{
			_to_int[data] = _ufs.size();        // 将泛型与数组下标之间建立映射
			_ufs.push_back(-1);
		}
	}
private:
	vector<int> _ufs;				// 保存元素之间关系的数组
	unordered_map<T, int> _to_int;  // 保存泛型与整数之间的转换的哈希桶
};

先定义出构造函数与添加元素的接口,注意并查集需要去重,如果不去重,元素与整数之间的转换就具有了歧义

template <class T>
class UnionFindSet
{
public:
	UnionFindSet(const T* arr, size_t n) // 用一个泛型数组初始化,并指定元素个数n
	{
		for (size_t i = 0; i < n; ++i)
		{
			add_elm(arr[i]);                 // 接口的复用
		}
	}
	
	UnionFindSet() = default;

	// 添加元素到并查集中
	void add_elm(const T& data)
	{
		// typename unordered_map<T, int>::iterator ret = _to_int.find(arr[i]);
		auto ret = _to_int.find(data);

		if (ret == _to_int.end())       // 并查集中没有这个元素才可以存储
		{
			_to_int[data] = _ufs.size();        // 将泛型与数组下标之间建立映射
			_ufs.push_back(-1);
		}
	}

	size_t find_root(size_t index)   // 查找index下标的根元素,返回其下标
	{
		size_t root = index;
		while (_ufs[root] >= 0)     // 根元素在并查集中的值是负数
		{
			root = _ufs[root];
		}

		while (_ufs[index] >= 0)
		{
			size_t parent = _ufs[index]; // 先保存其双亲,以对其双亲也进行路径压缩
			_ufs[index] = root;           // 路径压缩
			index = parent;
		}

		return root;
	}

	// 判断一个元素是否存在于并查集中,如果是返回其下标,否则返回-1
	size_t in_test(const T& data)
	{
		auto it = _to_int.find(data);

		if (it == _to_int.end()) // 如果元素不存在
		{
			return -1;
		}

		return it->second;
	}

	bool set_test(const T& data1, const T& data2)         // 判断两个元素是否在同一集合中
	{
		size_t index1 = in_test(data1);
		size_t index2 = in_test(data2);

		if (index1 == -1 || index2 == -1)                // 如果有一个元素不存在,抛异常
		{
			throw invalid_argument("set_test()::元素不存在");
		}

		return find_root(index1) == find_root(index2); // 返回两元素的根元素下标是否相等
	}

	// 连接两个集合
	void set_union(const T& data1, const T& data2)
	{
		size_t index1 = in_test(data1);
		size_t index2 = in_test(data2);

		if (index1 == -1 || index2 == -1)                // 如果有一个元素不存在,抛异常
		{
			throw invalid_argument("union()::元素不存在");
		}

		// 找到它们的根元素下标
		size_t root1 = find_root(index1);
		size_t root2 = find_root(index2);

		if (root1 != root2) // 不同集合才能合并
		{
			// 假设root1为根元素的集合元素个数更多,如果它的元素更少,交换
			// 注意根元素在并查集中存储的是负数
			if (_ufs[root1] > _ufs[root2])
			{
				swap(root1, root2);
			}

			_ufs[root1] += _ufs[root2]; // 数值的累加,维护集合的个数
			_ufs[root2] = root1;        // 将小集合作为大集合的子集,保存大集合根元素的下标
		}
	}

	size_t set_size() // 返回并查集中树的个数
	{
		// 遍历数组,只有有值小于0就说明它是一个根元素,树的个数加1
		size_t ret = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
			{
				ret++;
			}
		}
		return ret;
	}


	// for test
	void print()
	{
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			cout << _ufs[i] << ' ';
		}
		cout << endl;
	}
private:
	vector<int> _ufs;				// 保存元素之间关系的数组
	unordered_map<T, int> _to_int;  // 保存泛型与整数之间的转换的哈希桶
};

最后是所有接口的实现,由于前面已经讲过了逻辑,并且代码带有详细的注释,这里就不再赘述了。需要注意的是合并集合时需要先判断,只有不同集合才能合并,然后是需要将小集合合并到大集合中,如果反过来可能会出现某一路径过长的问题。除此之外,在查找一个元素的根节点时,可以强制的进行路径压缩,将该元素到其根元素之间的所有元素作为根的最近子元素,以提高查找根元素的效率

最后是demo的测试

int main()
{
	int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
	UnionFindSet<int> uset(arr, 10);
	uset.set_union(0, 6);
	uset.set_union(6, 7);
	uset.set_union(7, 8);
	uset.set_union(1, 4);
	uset.set_union(4, 9);
	uset.set_union(2, 3);
	uset.set_union(3, 5);
	cout << uset.set_test(3, 5) << endl;
	cout << uset.set_test(1, 5) << endl << endl;
	cout << uset.set_size() << endl << endl;
	uset.print();
}

在这里插入图片描述
测试的数据与之前讲解算法逻辑时用到的例子一样,经过大概的测试,模拟实现的并查集没有严重的bug

leetcode练习

省份数量

题目链接
在这里插入图片描述

首先,每个测试样例会给出一个n*n的二维矩阵,表示每个城市之间是否相连,并且一组相连的城市就是一个省份,问给定的二维数组中有几个省份?这不就是并查集中的树的数量吗?城市就是并查集中的元素,我们只要遍历二维数组,将相连的城市放到一个集合中,最后返回并查集中的树的数量即可

题目没有给出具体的城市名称,而是给出抽象的数字,用数组下标表示一个城市,所以并查集存储int就行了。首先,调用默认构造创建一个并查集,再调用add_ele接口初始化并查集,接着遍历二维数组调用set_union接口连接相连的城市,最后返回并查集中树的数量

int findCircleNum(vector<vector<int>>& isConnected) {
	UnionFindSet<int> uset;
	for (size_t i = 0; i < isConnected.size(); ++i)
	{
		uset.add_elm(i);
	}
	for (size_t i = 0; i < isConnected.size(); ++i)
	{
		for (size_t j = 0; j < isConnected[i].size(); ++j)
		{
			if (isConnected[i][j] == 1)
			{
				uset.set_union(i, j);
			}
		}
	}
	return uset.set_size();
}

在这里插入图片描述
上面这种解法需要现场搓一个并查集出来,但是只要掌握并查集的思想我们也能解出这道题,不用手搓一个并查集。由于题目直接用数字表示城市,所以我们可以不需要创建保存元素与整数之间映射关系的unordered_map,只用一个vector数组,数组的下标与城市是直接映射的关系。将vector数组resize(n)并给定初始值-1,然后写一个合并集合的lambda,遍历二维数组连接相连的城市,最后遍历我们创建的数组,有几个负数就有几个省份

int findCircleNum(vector<vector<int>>& isConnected) {
    vector<int> ufs(isConnected.size(), -1); // 并查集数组
    auto find_root = [&](size_t pos) {
        while (ufs[pos] >= 0)
        {
            pos = ufs[pos];
        }
        return pos;
    };
    auto set_union = [&](size_t pos1, size_t pos2) {
        size_t root1 = find_root(pos1);
        size_t root2 = find_root(pos2);
        if (root1 != root2)
        {
            ufs[root1] += ufs[root2];
            ufs[root2] = root1;
        }
    };
    for (size_t i = 0; i < isConnected.size(); ++i)
    {
        for (size_t j = 0; j < isConnected[i].size(); ++j)
        {
        	// 相连的城市放到同一集合中
            if (isConnected[i][j] == 1)
            {
                set_union(i, j);
            }
        }
    }
    // 树(省份)的统计
    size_t ret = 0;
    for (auto& v : ufs)
    {
        if (v < 0)
        {
            ret++;
        }
    }
    return ret;
}

在这里插入图片描述

等式方程的可满足性

在这里插入图片描述
这道题就是判断给定的字符串是否有自相矛盾的问题,我们先遍历一遍,把数值相等的字母放在一个集合中,然后再遍历一遍,判断不相等的字母有没有在同一集合中,如果有就说明示例矛盾,返回false。没啥好说的,这题只是多了一个是否处于同一集合的判断

class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        vector<int> ufs(26, -1); // 直接映射26个小写字母
        auto find_root = [&](size_t pos) {
        while (ufs[pos] >= 0)
        {
            pos = ufs[pos];
        }
        return pos;
    };
    auto set_union = [&](size_t pos1, size_t pos2) {
        size_t root1 = find_root(pos1);
        size_t root2 = find_root(pos2);
        if (root1 != root2)
        {
            ufs[root1] += ufs[root2];
            ufs[root2] = root1;
        }
    };
    auto set_test = [&](size_t pos1, size_t pos2){
        return find_root(pos1) == find_root(pos2);
    };

    for (auto& str : equations)
    {
        if (str[1] == '=')
        {
        	// 相等字母放入同一集合中
            set_union(str[0] - 'a', str[3] - 'a');
        }
    }
    for (auto& str : equations)
    {
    	// 判断不相等的字母是否在同一集合中,如果是,返回false
        if (str[1] == '!' && set_test(str[0] - 'a', str[3] - 'a') == true)
        {
            return false;
        }
    }
    return true;
    }
};

在这里插入图片描述

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

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

相关文章

【Maven】多环境配置与应用

目录 1. 多环境配置作用 问题导入 2. 多环境配置步骤 2.1 定义多环境 2.2 使用多环境&#xff08;构建过程&#xff09; 3. 跳过测试&#xff08;了解&#xff09; 问题导入 3.1 应用场景 3.2 跳过测试命令 3.3 细粒度控制跳过测试 1. 多环境配置作用 问题导入 多…

LeetCode 2331. 计算布尔二叉树的值

给你一棵 完整二叉树 的根&#xff0c;这棵树有以下特征&#xff1a; 叶子节点 要么值为 0 要么值为 1 &#xff0c;其中 0 表示 False &#xff0c;1 表示 True 。 非叶子节点 要么值为 2 要么值为 3 &#xff0c;其中 2 表示逻辑或 OR &#xff0c;3 表示逻辑与 AND 。 计算…

【推荐系统】User-Item CF:NGCF

&#x1f4a1; 本次解读的文章是 2019 年发表于 SIGIR 的一篇基于图卷积神经网络的用户物品协同过滤推荐算法论文&#xff0c; 论文将用户-物品交互信息建模为二分图&#xff0c;提出了一个基于二分图的推荐框架 Neural Graph Collaborative Filtering&#xff08;NGCF&#xf…

基于nodejs+vue的社区问答网站与设计

目 录 摘要 I Abstract II 1 绪论 1 1.1 选题背景 1 1.2 选题意义 1 1.3 研究内容 2 2 相关技术介绍 3 3 系统分析 5 3.1可行性分析 5 3.2 需求分析 5 3.2.1非功能性需求 5 3.2.2功能需求 6 3.3 系统用例 6 3.3.1 会员功能需求 6 …

【C++修炼之路】13. priority_queue及仿函数

每一个不曾起舞的日子都是对生命的辜负 stack&&queue一 . priority_queue介绍二. priority_queue的使用三. 仿函数3.1 仿函数的介绍3.2 仿函数的好处四.priority_queue模拟实现五.仿函数之日期比较一 . priority_queue介绍 priority_queue文档介绍 优先队列是一种容器…

机器学习实战(第二版)读书笔记(2)—— LSTMGRU

刚接触深度学习半年的时间&#xff0c;这期间有专门去学习LSTM &#xff0c;这几天读机器学习实战这本书的时候又遇到了&#xff0c;感觉写的挺好的&#xff0c;所以准备结合本书写一下总结方便日后回顾。如有错误&#xff0c;欢迎批评指正。 一、LSTM 优势&#xff1a;可在一…

ApiSix 开启SkyWalking插件,实现链路信息追踪

ApiSix 开启SkyWalking插件&#xff0c;实现链路信息追踪1 ApiSix开启SkyWalking插件1.1 修改config.yml配置文件1.2 在路由中开启SkyWalking插件2 创建两个SpringBoot服务&#xff0c;接入SkyWalking2.1 下载skywalking agent2.2 创建服务2.3 测试SkyWalking1 ApiSix开启SkyWa…

【链表】反转链表

BM1反转链表 描述 给定一个单链表的头结点pHead(该头节点是有值的&#xff0c;比如在下图&#xff0c;它的val是1)&#xff0c;长度为n&#xff0c;反转该链表后&#xff0c;返回新链表的表头。 数据范围&#xff1a; 0\leq n\leq10000≤n≤1000 要求&#xff1a;空间复杂度…

飞行员兄弟( 二进制枚举) --《算法竞赛进阶指南》

题目如下&#xff1a; 输入样例&#xff1a; --- ---- ---- ---输出样例&#xff1a; 6 1 1 1 3 1 4 4 1 4 3 4 4思路 or 题解&#xff1a; 数据量很小可以直接进行 搜索 在这里我使用 二进制枚举 的方法去寻找答案 时间复杂度&#xff1a;O(2n)O(2^n)O(2n) 我们二进制枚举…

计算机相关专业混体制的解决方案(事业编之学校与医院)

文章目录1、教师行业1.1 中小学教师资格1.2 高校教师资格证1.3 应聘中小学教师1.4 待遇2、医疗行业2.1 如何进入医院信息科2.2 医院信息科工作内容2.3 医院信息科待遇主要介绍三个方面&#xff1a; 1、招聘条件&#xff0c;要求是什么。 2、工作内容&#xff0c;需要我做什么工…

【哈希表】leetcode242.有效的字母异位词(C/C++/Java/Python/Js)

leetcode242.有效的字母异位词1 题目2 思路 &#xff08;字典解法&#xff09;3 代码3.1 C版本3.2 C版本3.3 Java版本3.4 Python版本3.5 JavaScript版本4 总结1 题目 题源链接 给定两个字符串 s 和 t &#xff0c;编写一个函数来判断 t 是否是 s 的字母异位词。 注意&#xf…

【JavaScript】多态(Symbol),迭代器接口,getter/setter,继承,instanceof

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录多态symbol迭代器接口实现数组的迭代器方法练习Getter/Setterstatic继承instanceof多态 我拿到一…

虽迟但到,我的2022年终总结

“子在川上曰&#xff0c;逝者如斯夫”。经历一年的居家办公、解封&#xff0c;终于在年底回归正常生活。时隔三年回老家过年&#xff0c;一切如故&#xff0c;好像疫情从没有来过。2022年对我来说是值得记忆的一年&#xff0c;在这一年里完成了买房这件人生大事&#xff0c;终…

SpringCloud学习笔记【part1】Spring Cloud Gateway网关的搭建、处理跨域问题

一、Spring Cloud Gateway 介绍 API网关出现的原因是微服务架构的出现&#xff0c;不同的微服务一般会有不同的网络地址&#xff0c;而外部客户端可能需要调用多个服务的接口才能完成一个业务需求。API 网关是介于客户端和服务器端之间的中间层&#xff0c;所有的外部请求都会先…

macos nginx 安装/卸载,启动/重启/关闭

1.安装/卸载 在macos中使用homebrew安装 brew install nginx,卸载brew uninstall nginx。homebrew安装 brew install xx&#xff0c;卸载brew uninstall xx 可以直接安装nginx-full&#xff0c;来安装echo模块 brew install nginx-full # 如果上面安装报错&#xff0c;需要按…

C 程序设计教程(18)—— 数组和指针(一):数组

C 程序设计教程&#xff08;18&#xff09;—— 数组和指针&#xff08;一&#xff09;&#xff1a;数组 该专栏主要介绍 C 语言的基本语法&#xff0c;作为《程序设计语言》课程的课件与参考资料&#xff0c;用于《程序设计语言》课程的教学&#xff0c;供入门级用户阅读。 目…

【SpringCloud复习巩固】微服务+Eureka+Ribbon

文章中需要用到的代码和sql 链接&#xff1a;https://pan.baidu.com/s/1_1Qqro7wR5zi7Ds8Bgmf-g 提取码&#xff1a;vxzg 目录 一.微服务 1.1单体架构 1.2分布式架构 1.3微服务 1.4各自特点总结 1.5微服务技术对比 二.服务拆分及远程调用 三.Eureka注册中心 3.1服务调用…

KVM虚拟化之小型虚拟机kvmtool的使用

根据 kvmtool github仓库文档的描述&#xff0c;类似于QEMU&#xff0c;kvmtool是一个承载KVM Guest OS的 host os用户态虚拟机&#xff0c;作为一个纯的完全虚拟化的工具&#xff0c;它不需要修改guest os即可运行, 不过&#xff0c;由于KVM基于CPU的硬件虚拟化支持&#xff0…

【JavaGuide面试总结】Java集合篇·上

【JavaGuide面试总结】Java集合篇上1.简单说说Java集合框架体系Collection接口Map接口2.说说 List, Set, Queue, Map 四者的区别&#xff1f;3.你在编程时如何选用集合?4.Collection 子接口之 ListArrayList 和 Vector 的区别?ArrayList 与 LinkedList 区别?说一说 ArrayLis…

ISIS特性与配置实例(DU比特、OL置位、Tag等)

2.3.0 ISIS基础命令与查询命令介绍、ISIS特性、ISIS与OSPF 本文章以ISIS的特性结合ISIS配置实例进行讲述&#xff0c;故篇幅会非常之大&#xff0c;建议电脑端上结合目录观看以免影响观感。 目录路由渗透配置路由渗透实例一、配置设备IP地址二、配置ISIS三、检查ISIS&#xff0…