【数据结构】并查集

👦个人主页:Weraphael

✍🏻作者简介:目前正在准备26考研

✈️专栏:数据结构

🐋 希望大家多多支持,咱一起进步!😁

如果文章有啥瑕疵,希望大佬指点一二

如果文章对你有帮助的话

欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


目录

一、并查集原理

在一些应用问题中,我们需要将n个不同的元素划分成若干个不相交的集合。开始时,每个元素自成一个单独的集合,然后根据一定的规律将元素归 到同一集合中。在此过程中,我们需要频繁地 询某个元素属于哪个集合。用于描述这类问题的抽象数据结构叫做并查集(Union-Find Set)。并查集的本质是一个森林,其中每棵树表示一个集合。

比如:某公司今年校招全国总共招生10人,西安招4人,成都招3人,武汉招3人,10个人来自不

同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]。 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个

数。(负号下文解释)

毕业后,学生们要去公司上班,每个地方的学生自发组织成小分队一起出发,于是,西安学生小分队s1 = [0, 6, 7, 8]、成都学生小分队s2 = [1, 4, 9]和武汉学生小分队s3 = [2, 3, 5]就相互认识了。这样,10个人被分成三个小团体。假设这三个小团体的队长分别是0(西安队)、1(成都队)和2(武汉队),他们负责带领大家的出行。

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

从上图可以看出:编号678同学属于0号小分队,该小分队中有4人(包含队长0);编号为49的同学属于1号小分队,该小分队有3人(包含队长1),编号为35的同学属于2号小分队,该小分队有3个人(包含队长1)。

仔细观察数组中内变化,可以得出以下结论:

  • 数组的下标对应集合中元素的编号
  • 数组中如果为负数,负号代表根,数字代表该集合中元素个数
  • 数组中如果为非负数,代表该元素所属父结点在数组中的下标

说明:下面实现代码的时候就会用到以上结论 ~

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

现在0集合有7个人,2集合有3个人,总共两个朋友圈。

通过以上例子可知,并查集一般可以解决一下问题:

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

二、并查集实现

2.1 初始化

初始时,每个元素都是独立的个体(各自是一个集合),即父节点都是自己,且每个元素的集合大小为1。因此,可以将数组元素全部初始化成-1

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

private:
	std::vector<int> _ufs;
};

2.2 合并操作

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

这个要怎么合并呢?首先事先说明一下,集合1合并到集合0是可以的,当然集合0合并到集合1也是可以的,没有硬性要求。

我们这就就以集合1合并到集合0为例,那么我们首先需要找到8的根,也就是0。那么这个就很简单了,因为我们一开始规定元素 < 0为根,如果该元素是负数,表示它是根节点;反之是正数,那么就继续递归寻找,知道找到负数位置。

cpp 复制代码
class UnionFindSet
{
public:
	// 合并操作
	void Union(int x1, int x2)
	{
		// TODO
	}

	// 找父亲
	int FindRoot(int x) 
	{
		int parent = x;
		// 如果数组中存储的是负数,说明找到根(父亲)
		// 否则继续找
		while (_ufs[parent] >= 0)
		{
			parent = _ufs[parent];
		}
		return parent; // 返回父亲的下标
	}

private:
	std::vector<int> _ufs;
};

接下来就是完善合并操作,步骤如下:

  1. 先分别找到两个元素x1x2的根。
  2. 再判断x1x2是否在同一个集合里,如果在同一个集合里就没必要合并了。判断是否在同一个集合也很简单,只需判断两个元素的根是否相等即可。
  3. 最后将根与根之间进行合并即可。这里需要做两件事,假设是x2这个所在集合合并到x1所在集合,
    • 那么就要增加x1所在集合的个数。
    • 并且x2的根要修改成x1所在集合的根。
cpp 复制代码
class UnionFindSet
{
public:
	// 合并操作
	void Union(int x1, int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		// 接下来判断它们是不是在同一个集合
		
		// 在同一个集合的话,就没有必要合并
		if (root1 == root2) return;
		else // 不在同一个集合
		{
			// 将x2合并到x1的集合中
			_ufs[root1] += _ufs[root2];
			_ufs[root2] = root1;
		}
	}
	
	// 找父亲
	int FindRoot(int x) 
	{
		int parent = x;
		// 如果数组中存储的是负数,说明找到根(父亲)
		// 否则继续找
		while (_ufs[parent] >= 0)
		{
			parent = _ufs[parent];
		}
		return parent; // 返回父亲的下标
	}

private:
	std::vector<int> _ufs;
};

2.3 判断在不在同一个集合里

这个就之间判断两个元素的根是否相等即可

cpp 复制代码
class UnionFindSet
{
public:
	// 判断在不在同一个集合里
	bool IsInSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}

private:
	std::vector<int> _ufs;
};

2.4 集合的个数

计算集合的个数本质上就是统计有几颗树,那么有几个根就代表有几个数,又因为根的特征是负值,那么就判断有几个负数不就行了~

cpp 复制代码
class UnionFindSet
{
public:
	// 计算集合的个数(几颗树)
	size_t SetSize()
	{
		// 统计有几个负数即可
		size_t count = 0;
		for (auto e : _ufs)
		{
			if (e < 0)
				count++;
		}
		return count;
	}

private:
	std::vector<int> _ufs;
};

三、优化:路径压缩

比方说有如上集合,要查找9的根,那么需要查找三次。如果该集合的数据量非常大呢?那么查找的效率就非常的低了。因此,路径压缩是用于优化查找操作的技术,旨在加速后续的查询过程。它的作用是通过扁平化树的结构,使得每个节点的父节点直接指向根节点,从而减少树的高度,提高查找操作的效率。

以下是路径压缩后的结果

代码的话仅需在查找的过程中进行路径压缩。

压缩的方法很简单,就是在寻找某一个结点的根节点的时候,由于是向上寻找的,那么就可以沿着这条路径,将这条路径上的所有结点的父亲都改为2,也就是根节点。因为从结果上来说,他们的根节点都是一样的。

cpp 复制代码
// 查找根节点,并进行路径压缩
int FindRoot(int x)
{
	// 寻找根节点
	if (_ufs[x] < 0)
		return x;  // 如果x是根节点,直接返回
	else
	{
		// 路径压缩:递归查找父节点并将当前节点指向根节点
		_ufs[x] = FindRoot(_ufs[x]);  // 压缩路径
		return _ufs[x];  // 返回根节点
	}
}

四、并查集应用

4.1 省份数量

这道题的大致意思是:比方有A,B,C这三个城市,城市和城市之间可以是直接相连(城市A可以直接到达城市B),也可以是间接相连的(城市B可以直接到达城市C,那么可以说城市A和城市C是间接相连的)。那么这道题用并查集来解决就相当简单了,如果isConnected[i][j] == 1表示城市i和城市j是直接相连的,那么他们就同属一个集合中,反之不属于。最后再返回有多少个集合即可。

参考代码如下:

cpp 复制代码
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++)
            {
                // 值是1,表示城市i与城市k之间是直接相连,
                // 那么 i 和 j 就属于同一个集合
                if (isConnected[i][j] == 1)
                {
                    ufs.Union(i, j);
                }
            }
        }
        return ufs.SetSize();
    }
};

在实际中,我们不可能会直接手撕一个并查集,这样太过于麻烦了,我们只需要利用它的思想即可。如下代码所示

cpp 复制代码
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++)
            {
                // 值是1,表示城市i与城市k之间是直接相连,
                // 那么 i 和 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 n = 0;
        for (auto e : ufs)
        {
            if (e < 0)
            {
                n++;
            }
        }
        return n;
    }
};

4.2 等式方程的可满足性

这道题的意思就是让我们判断equations中的字符串等式是否相违背。比方说示例1,第一个是a == b,然后又给出b != a,这就相违背了。那么这道题可以使用并查集,如果所给的等式都在一个集合里,那么就是不违背,只要有一个不在集合里,就是违背的。步骤如下:

  1. 将所有==两端的字符合并到一个集合中
  2. 检测!=两端的字符是否在同一个结合中,如果都在,就是不满足;反之满足。
cpp 复制代码
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& 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;
                }
            }
        }
        // 第二遍,看看不相等是不是的两端元素在不在同一个集合中
        for (auto& str : equations) 
        {
            if (str[1] == '!') 
            {
                int root1 = findRoot(str[0] - 'a');
                int root2 = findRoot(str[3] - 'a');
                if (root1 == root2) // 如果在返回false
                {
                    return false;
                }
            }
        }
        return true;
    }
};

五、代码仓库

Gitee链接:点击跳转

相关推荐
bbqz00724 分钟前
浅说c/c++ coroutine
c++·协程·移植·epoll·coroutine·libco·网络事件库·wepoll
DB_UP24 分钟前
长时间序列预测算法---Informer
人工智能·python·算法
~糖炒栗子~26 分钟前
[Day 11]209.长度最小的子数组
数据结构·c++·算法·leetcode
L73S3727 分钟前
数据结构、算法与STL
数据结构·笔记·程序人生·算法
A_Tai233333329 分钟前
动态规划解决整数拆分问题
算法·动态规划
进击的小小学生40 分钟前
Python 中的 VWAP 算法策略
开发语言·python·算法
liang89991 小时前
java实现一个kmp算法
java·开发语言·算法
zym大哥大1 小时前
C++11右值与列表初始化
数据结构·c++
KeyPan1 小时前
【视觉SLAM:八、后端Ⅱ】
人工智能·深度学习·数码相机·算法·机器学习
qystca2 小时前
洛谷 P1075 [NOIP2012 普及组] 质因数分解 C语言
c语言·数据结构·算法