👦个人主页:Weraphael
✍🏻作者简介:目前正在准备
26
考研✈️专栏:数据结构
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、并查集原理
- 二、并查集实现
-
-
- [2.1 初始化](#2.1 初始化)
- [2.2 合并操作](#2.2 合并操作)
- [2.3 判断在不在同一个集合里](#2.3 判断在不在同一个集合里)
- [2.4 集合的个数](#2.4 集合的个数)
-
- 三、优化:路径压缩
- 四、并查集应用
-
-
- [4.1 省份数量](#4.1 省份数量)
- [4.2 等式方程的可满足性](#4.2 等式方程的可满足性)
-
- 五、代码仓库
一、并查集原理
在一些应用问题中,我们需要将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
(武汉队),他们负责带领大家的出行。
一趟火车之旅后,每个小分队成员就互相熟悉,称为了一个朋友圈。
从上图可以看出:编号6
,7
和8
同学属于0
号小分队,该小分队中有4
人(包含队长0
);编号为4
和9
的同学属于1
号小分队,该小分队有3
人(包含队长1
),编号为3
和5
的同学属于2
号小分队,该小分队有3
个人(包含队长1
)。
仔细观察数组中内变化,可以得出以下结论:
- 数组的下标对应集合中元素的编号
- 数组中如果为负数,负号代表根,数字代表该集合中元素个数
- 数组中如果为非负数,代表该元素所属父结点在数组中的下标
说明:下面实现代码的时候就会用到以上结论 ~
在公司工作一段时间后,西安小分队中8
号同学与成都小分队1
号同学奇迹般的走到了一起,两个小圈子的学生相互介绍,最后成为了一个小圈子:
现在0
集合有7
个人,2
集合有3
个人,总共两个朋友圈。
通过以上例子可知,并查集一般可以解决一下问题:
- 查找元素属于哪个集合:沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)。
- 查看两个元素是否属于同一个集合:沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在。
- 将两个集合归并成一个集合
- 将两个集合中的元素合并
- 将一个集合名称改成另一个集合的名称
- 集合的个数:遍历数组,数组中元素为负数的个数即为集合的个数。
二、并查集实现
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;
};
接下来就是完善合并操作,步骤如下:
- 先分别找到两个元素
x1
和x2
的根。 - 再判断
x1
和x2
是否在同一个集合里,如果在同一个集合里就没必要合并了。判断是否在同一个集合也很简单,只需判断两个元素的根是否相等即可。 - 最后将根与根之间进行合并即可。这里需要做两件事,假设是
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
,这就相违背了。那么这道题可以使用并查集,如果所给的等式都在一个集合里,那么就是不违背,只要有一个不在集合里,就是违背的。步骤如下:
- 将所有
==
两端的字符合并到一个集合中 - 检测
!=
两端的字符是否在同一个结合中,如果都在,就是不满足;反之满足。
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
链接:点击跳转