并查集的原理
在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个
单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一
个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find
set)。
并查集底层通常常用数组存储,并且存在以下特性:
- 数组的下标对应集合中元素的编号
- 数组中如果为负数,负号代表根,数字代表该集合中元素个数
- 数组中如果为非负数,代表该元素双亲在数组中的下标
这里举一个例子,假设今年秋季大一开学时班级共有10名学生,上海3人,海南4人,广东3人。这些学生都是刚进入校园,彼此都不认识,一开始每一个学生都是一个独立的小团体,现在用下标来为学生们编号,顺序按照上面的所在地。
因为来自同一个地方的学生他们从小的风俗习惯都十分接近,并且也有共同话题,所以很快他们就形成了自己的小团体
紧接着,因为来自上海的2号同学和来自海南的5号同学是同一个宿舍的,所以他也很快融入了2号的朋友圈,并且把自己的朋友也介绍给2号,所以此时上海和海南的朋友圈集合开始交集
通过这个结构,可以确认此时存在着两个集合,集合上海-海南共有7人,集合背景共有3人。
通过这种并查集的结构,可以用来解决以下问题
- 查找元素属于哪个集合
- 查看两个元素是否属于同一个集合
- 将两个集合归并成一个集合
- 集合的个数
并查集的实现
class UnionFindSet
{
public:
//初始化时将所有元素置为-1,表示每一个都是独立的集合
UnionFindSet(size_t size)
: _ufs(size, -1)
{}
//找到某一个集合的根
int FindRoot(int index)
{
//根节点的值为负数,如果不为负则说明不是根节点,继续找
while (_ufs[index] >= 0)
{
//因为每个节点存储自己父节点的下标,所以往上继续查找
index = _ufs[index];
}
return index;
}
//将两个集合并集
bool Union(int x1, int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
//本来就是一个集合,不需要合并
if (root1 == root2)
{
return false;
}
//选择root1作为根,将root2所在集合合并到root1中
_ufs[root1] += _ufs[root2];
//将另一个的父节点设置为root1
_ufs[root2] = root1;
return true;
}
//求集合的个数,即负节点的个数
size_t count() const
{
size_t count = 0;
for (auto it : _ufs)
{
if (it < 0)
{
count++;
}
}
return count;
}
private:
std::vector<int> _ufs;
};
例题
并查集一般可以解决以下问题:
- 查找元素属于哪个集合
- 查看两个元素是否属于同一个集合
- 将两个集合归并成一个集合
- 集合的个数
例如leetcode-547.朋友圈就属于上面的第四种问题,也就是我最开始举例的那个问题
class Solution {
public:
int findCircleNum(vector<vector<int>>& M) {
UnionFindSet ufs(M.size());
/*
M[i][j] = x 代表着i同学和j同学之间的关系为x
*/
for(int i = 0; i < M.size(); i++)
{
for(int j = 0; j < M[i].size(); j++)
{
//如果为朋友,则将两人并集
if(M[i][j] == 1)
{
ufs.Union(i, j);
}
}
}
return ufs.count();
}
};
leetcode-990. 等式方程的可满足性就属于上面的第二类问题
主要思路就是判断不等集合是否与相等集合产生交集
/*
思路:两次遍历数据,第一次将所有相等集合并集。第二次查找相等集合中是否有数据存在于不等集合,如果有则说明产生互斥,返回false
*/
class Solution {
public:
bool equationsPossible(vector<string>& equations) {
UnionFindSet ufs(26);
for(const auto& str : equations)
{
//将相等的数据放入集合中
if(str[1] == '=')
{
ufs.Union(str[0] - 'a', str[3] - 'a');
}
}
for(const auto& str : equations)
{
//判断不等的两端是否处于集合,如果有则说明产生互斥,不满足条件,返回false
if(str[1] == '!')
{
if(ufs.FindRoot(str[0] - 'a') == ufs.FindRoot(str[3] - 'a'))
{
return false;
}
}
}
return true;
}
};