目录
1. 并查集原理
在一些应用问题中,需要将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)。
仔细观察数组中内融化,可以得出以下结论:
1. 数组的下标对应集合中元素的编号
2. 数组中如果为负数,负号代表根,数字代表该集合中元素个数
3. 数组中如果为非负数,代表该元素双亲在数组中的下标
在公司工作一段时间后,西安小分队中8号同学与成都小分队1号同学奇迹般的走到了一起,两个小圈子的学生相互介绍,最后成为了一个小圈子:
现在0集合有7个人,2集合有3个人,总共两个朋友圈。
通过以上例子可知,并查集一般可以解决以下问题:
1. 查找元素属于哪个集合
沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)
2. 查看两个元素是否属于同一个集合
沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在
3. 将两个集合归并成一个集合
将两个集合中的元素合并
将一个集合名称改成另一个集合的名称
4. 集合的个数
遍历数组,数组中元素为负数的个数即为集合的个数。
2. 并查集实现
class UnionFindSet { public: UnionFindSet(size_t n) :_ufs(n, -1) {} void Union(int x1,int x2) { int root1 = FindRoot(x1); int root2 = FindRoot(x2); //如果本身就在一个集合就没必要合并了 if (root1 == root2) return; //可以加也可以不加,数据量少的往大的里面加 if (abs(_ufs[root1]) > abs(_ufs[root2])) swap(root1, root2); _ufs[root1] += _ufs[root2]; _ufs[root2] = root1; } int FindRoot(int x) { int root = x; while (_ufs[root] >= 0) { root = _ufs[root]; } //路径压缩 while (_ufs[x] >= 0) { int parent = _ufs[x]; _ufs[x] = root; x = parent; } return root; } bool InSet(int x1,int x2) { return FindRoot(x1) == FindRoot(x2); } size_t SetSize() { size_t size = 0; for (size_t i = 0; i < _ufs.size(); i++) { if (_ufs[i] < 0) { ++size; } } return size; } private: vector<int> _ufs; };
上面为总代码,我接下来将每个部分进行详细讲解。
2.1成员变量
private: vector<int> _ufs;
根据我们之前的理论,我们的成员变量只需要有一个vector就够了
2.2构造函数
UnionFindSet(size_t n) :_ufs(n, -1) {}
这里我们给我们的并查集开n个空间初始值设置为-1就可以了。
2.3合并
void Union(int x1,int x2) { int root1 = FindRoot(x1); int root2 = FindRoot(x2); //如果本身就在一个集合就没必要合并了 if (root1 == root2) return; //可以加也可以不加,数据量少的往大的里面加 if (abs(_ufs[root1]) > abs(_ufs[root2])) swap(root1, root2); _ufs[root1] += _ufs[root2]; _ufs[root2] = root1; }
我们先找出指定的两个元素的根,如果是同一个那就没有必要合并了,否则的话我们就将第二个元素归到第一个元素之下,更新他们两个的下标就好了。当然我中间还写了一步压缩路径,这个其实一般用不到,但如果有这方面的需求的话我们还是可以写一下。
压缩路径:我们为什么需要压缩路径呢?主要是为了查找这个元素的根的时候性能更高。
如上图所示我们如果要找3的根也就是0的话就要经过4次循环,倘若数据量非常大,那么查找根的效率可能就会变低,所以我们可以采用合并期间就压缩路径的方式来提高我们的代码的效率。
在这里我们的压缩路径的方法是判断两个集合的根的下标,谁大谁就被合并。这个函数里的路径压缩其实可有可无,因为效果不是很明显,但我们下一个找根的函数的逻辑运算就比较有用。
2.4查找根
int FindRoot(int x) { int root = x; while (_ufs[root] >= 0) { root = _ufs[root]; } //路径压缩 while (_ufs[x] >= 0) { int parent = _ufs[x]; _ufs[x] = root; x = parent; } return root; }
查找根的思路很简单,我们只需要来个循环一直找当前元素的父亲元素,直到下标为<0即可。我们这里的路径压缩就比较有效果了,我们在查找的这个过程中把被一个父亲元素都归到根的下边,因为在一个集合中除了根,其他元素的地位都是一样的,我们可以随意更改其路径。
比如我们找上面的图中5的根就可以压缩成如下图所示:
这样我们不管查谁都是O(1)的复杂度。
2.5判断是否在同一个集合
bool InSet(int x1,int x2) { return FindRoot(x1) == FindRoot(x2); }
这个非常简单,我们直接复用FindRoot函数就可以,如果相等他们就在,不相等就不在。
2.6计算集合的个数
size_t SetSize() { size_t size = 0; for (size_t i = 0; i < _ufs.size(); i++) { if (_ufs[i] < 0) { ++size; } } return size; }
这个也非常简单,我们只需要遍历vector,如果是<0的,就说明它是并查集的头,就size++,最后再返回就可以了。
3. 并查集应用
LeetCode:LCR 116. 省份数量
题目解析:
这道题的意思没有表达的很明确,它大概的意思就是计算有多少个集合(省份),这道题我们使用我们刚刚写好的并查集就可以解决了。
class UnionFindSet { public: UnionFindSet(size_t n) :_ufs(n, -1) {} void Union(int x1,int x2) { int root1 = FindRoot(x1); int root2 = FindRoot(x2); //如果本身就在一个集合就没必要合并了 if (root1 == root2) return; //可以加也可以不加 if (root1 > root2) swap(root1, root2); _ufs[root1] += _ufs[root2]; _ufs[root2] = root1; } int FindRoot(int x) { int parent = x; while (_ufs[parent] >= 0) { parent = _ufs[parent]; } return parent; } bool InSet(int x1,int x2) { return FindRoot(x1) == FindRoot(x2); } size_t SetSize() { size_t size = 0; for (size_t i = 0; i < _ufs.size(); i++) { if (_ufs[i] < 0) { ++size; } } return size; } private: vector<int> _ufs; }; class Solution { public: int findCircleNum(vector<vector<int>>& isConnected) { size_t n = isConnected.size(); UnionFindSet ufs(n); for(size_t i = 0;i<isConnected.size();i++) { for(size_t j=0;j<isConnected[i].size();j++) { if(isConnected[i][j]==1) { if(!ufs.InSet(i,j)) ufs.Union(i,j); } } } return ufs.SetSize(); } };
我们就看Solution类里面的内容,我们的思路很简单,只需要构造一个并查集,然后将相邻的还未合并的省合并到一个集合即可,最后返回我们的集合个数就可以了。当然我们也可以不用真的弄一个并查集,我们只需自己手动写一下它的核心功能也可以解决这道题目,这种解法我放下一道题来讲。
LeetCode:990. 等式方程的可满足性
题目解析:
这道题的意思就是如果有3个数,其中两对是相等的,那么他们三个数之间都相等,这就跟我们并查集的集合思路是一致的,我们这次不复用我们的并查集类直接解决它。
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(int i = 0;i<equations.size();i++) { int x1 = equations[i][0]-97,x2 = equations[i][3]-97; if(equations[i][1]=='=') { int root1=FindRoot(x1); int root2=FindRoot(x2); if(root1!=root2) { ufs[root1]+=ufs[root2]; ufs[root2]=root1; } } } for(int i = 0;i<equations.size();i++) { int x1 = equations[i][0]-97,x2 = equations[i][3]-97; if(equations[i][1]=='!') { int root1=FindRoot(x1); int root2=FindRoot(x2); if(root1==root2) return false; } } return true; } };
首先,我们需要一个vector来当我们的并查集结构,其次我们需要的第一个功能函数就是找根,我们可以使用lambda表达式来完成它,函数的设计跟我们的并查集类里面的是一模一样的,然后,我们遍历题目里面所提供的vector,我们先找出所有==的数据,将这两个数进行我们的合并操作,合并的操作也是一模一样的,合并完成之后我们再遍历一次,将!=的两个数进行判断,判断他们两个在不在一个集合中,实际就是判断他们的根是否相等,如果相等,我们就直接返回false,因为这两个数本来不因该在一个集合中,结果查找根他们却在同一个集合中,这就不符合要求,我们直接返回false就可以了,在这趟循环结束后,那自然是所有数据都满足情况了,这时候我们就可以返回true了。
07-01
197
