前提:并查集的概念
并查集作为一种数据结构在处理需要将n个元素划分成不相交的集合,在逐步按照某一种规律将某些元素给合并,并在这个过程中要反复查询某一种元素归属于哪个集合的运算,称之为并查集
并查集的用途
这道题讲述就是有一组元素,他们相互之间产生某种关系,形成了集合,面对这种问题,常规的解法弄来弄去可能都会觉得做的不太对,但有了并查集我们解决这类问题是较简单的。
并查集的结构解释
注释:双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点
我们简要的举个例子,这里我们有5人,他们的编号为 0 - 4,假设他们是一群准大一的学生,他们刚开始各自为一个集合。
我们这里用负数表示自己为整体,索引指向的内容若为正数表示该索引存放的值为集合当中的双亲结点。
假设过了一段时间后,编号为0,2,4的同学形成了一个集合。编号为1,3的同学形成了一个集合。,如下图所示
这里的0,1分别是一个集合,索引对应的值是集合中的元素个数的相反数
解释: 此时父节点中存放的是集合大小的负值,子节点存放的是父节点的索引,倘若这时编号为4 与编号为3的结点因为一场比赛结识成为了朋友,我们要如何做呢? - -实际上,当4与3结合成朋友,自然两个集合中的每一个人都成了朋友,我们这时只需要让其中一个集合成为父节点,两个集合便成了一个集合了 。
此时我们的人形成了一个整体,每个子节点的存放的都是父节点的下标(0),0位置存放结点的个数的负值(因为我们用负数来区分是否为根)
我们也可以看出来一个并查集所需要具备哪些结论?
1.数组的下标对应集合中的元素下标
2.负数代表根,数字代表该集合中的元素个数
3.若数组中为非负数,则为父节点在数组当中的下标
我们也可以看出来一个并查集所需要具备哪些功能?
1.查找元素属于哪一个集合
2.查找两个元素是否为一个集合
3.两个元素合并成一个集合
4.集合的个数
并查集的实现
注释中有解释,详情请看注释,并结合下面的两道习题,其实就是实现了上面概括的四个接口
#include<iostream>
using namespace std;
#include<vector>
#include<assert.h>
class UnionFindSet
{
public:
//构造函数,初始化开空间
UnionFindSet(int x)
{
_ufs.resize(x, -1);
//初始化的时候讲x个空间初始化成-1
}
//合并两个元素为集合
bool Union(int x1,int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (root1 == root2)
return false;
else
{
//将root1成为新集合的头,并计算他的元素个数,root2的内容则指向root1的索引
_ufs[root1] += _ufs[root2];
_ufs[root2] = root1;
return true;
}
}
int FindRoot(int x)
{
assert(x < _ufs.size());
while (_ufs[x] >= 0)
{
x = _ufs[x];
}
return x;
}
//算出数组当中有多少个集合
int Size()
{
int count = 0;
for (int i = 0; i < _ufs.size(); ++i)
{
//计算数组中有几个集合
if (_ufs[i] < 0)
count++;
}
return count;
}
private:
vector<int> _ufs;
};
并查集相关习题
习题1.leetcode 朋友圈
分析: 这题其实就是给了我们一个二维矩阵,然后让我们确定有多少个朋友圈,这个就很简单了,我们可以遍历一遍数组,用我们的Union的接口将两个人合并成一个集合,最后在用Size接口来计算集合的个数。
class UnionFindSet
{
public:
UnionFindSet(int x)
{
_ufs.resize(x, -1);
//初始化的时候讲x个空间初始化成-1
}
bool Union(int x1,int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (root1 == root2)
return false;
else
{
//将root1成为新集合的头,并计算他的元素个数,root2的内容则指向root1的索引
_ufs[root1] += _ufs[root2];
_ufs[root2] = root1;
return true;
}
}
int FindRoot(int x)
{
assert(x < _ufs.size());
while (_ufs[x] >= 0)
{
x = _ufs[x];
}
return x;
}
int Size()
{
int count = 0;
for (int i = 0; i < _ufs.size(); ++i)
{
if (_ufs[i] < 0)
count++;
}
return count;
}
private:
vector<int> _ufs;
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
UnionFindSet ufs(isConnected.size());
for(int i =0;i<isConnected.size();++i)
{
for(int j =0;j<isConnected.size();++j)
{
//遍历二维数组,将 i==j自己与自己的情况过滤
if(i == j)
continue;
//将为朋友的弄成集合
if(isConnected[i][j]==1)
ufs.Union(i,j);
}
}
int ret = ufs.UfsSize();
return ret;
}
};
习题2.leetcode 等式方程的可满足性
分析:这道题目也是用并查集实现为最优解,我们这道题可以先遍历一遍数组,将所有string中的第二个位置为 ‘=’先放入并查集当中,在遍历一遍从第二个位置’!'查看是否有重复的错误逻辑放入
class UnionFindSet
{
public:
UnionFindSet(int x)
{
_ufs.resize(x, -1);
//初始化的时候讲x个空间初始化成-1
}
bool Union(int x1,int x2)
{
int root1 = FindRoot(x1);
int root2 = FindRoot(x2);
if (root1 == root2)
return false;
else
{
//将root1成为新集合的头,并计算他的元素个数,root2的内容则指向root1的索引
_ufs[root1] += _ufs[root2];
_ufs[root2] = root1;
return true;
}
}
int FindRoot(int x)
{
assert(x < _ufs.size());
while (_ufs[x] >= 0)
{
x = _ufs[x];
}
return x;
}
int Size()
{
int count = 0;
for (int i = 0; i < _ufs.size(); ++i)
{
if (_ufs[i] < 0)
count++;
}
return count;
}
private:
vector<int> _ufs;
};
class Solution {
public:
bool equationsPossible(vector<string>& equations) {
UnionFindSet ufs(26);
//先遍历一遍,将所有==的情况的集合都找出来,再第二次遍历将!=看==的情况是否存在
for(auto& e: equations)
{
//第一次遍历,建立映射
if(e[1] =='=')
ufs.Union(e[0]-'a',e[3]-'a');
}
for(auto& e:equations)
{
if(e[1] =='!')
{
//第二次遍历,查错
//若他们的头结点为同一个,说明在一个集合,则与逻辑错误
if(ufs.FindRoot(e[0]-'a') == ufs.FindRoot(e[3]-'a'))
return false;
}
}
//当走到这里的时候,表示并查集中没有出现逻辑错误的
return true;
}
};
并查集的思考与提升
这里大家思考一下,朋友圈这样的题型,倘若他给我们的不是编号,而是人名,那么我们如何利用我们的并查集呢,我们的并查集都是通过下标来实现的。
我们这里可以用vector< string > nameV={“小明”,“小红”,“小绿”…},这样子我们就可以通过下标找到对应的人。
我们用人找下标就可以用map<string,int> nameIndexmap 来通过人名找到对应下标,这样我们的并查集就可以实现通用了
通用分析:
map 可以由名字找到下标_ufs[下标] 可以找到对应的头结点
vector 可以由下标找到人
我们想要进行人名对应查找头节点的时候,map(人名)首先可以找到下标,vector[下标]可以找到对应的双亲结点的人,相当于找到了对应的双亲结点的名字,我们就可以利用这种结构对_ufs这个数组进行更改
总结
- 喜欢就收藏
- 认同就点赞
- 支持就关注
- 疑问就评论