什么是并查集? 逐字拆解一下,并、查、集。这个三个字,其中前面两个字都是动词,第三个字是个名词。
先看名词,因为只有知道了这个东西是什么,才能去理解它能干什么。
集就是集合,中学时期就学过这个东西,集合用大白话说就是将一堆元素没有顺序地摆放在同一个地方。
其实并查集本质就是集合。
那它能做什么呢?这就要看前两个字 - “并” 和 “查”。
集合的一些操作,例如,交集,并集等等,这里的 “并” 其实就指的是并集操作,两个集合合并后就会变成一个集合。例如:
{1,3,5,7} U {2,4,6,8} = {1,2,3,4,5,6,7,8}
那 “查” 又是什么呢?集合本身只是容器,最终还是要知道里面存的是什么元素,因此这里的 “查” 是对于集合中存放的元素来说的,即要查找这个元素在不在集合中,还要确定这个元素是在哪个集合中。
好了,现在知道并查集是什么,以及它能干什么了,总结下来就是:
并查集可以进行集合合并的操作(并) 并查集可以查找元素在哪个集合中(查) 并查集维护的是一堆集合(集) 举个例子:
有8个元素:14, 35, 48, 87, 65, 20。
集:把个位相同的数字归在同一个集合。集合划分为如下:
{14}, {35, 65}, {48}, {87}, {20}
查:给定一个元素,得到这个元素属于哪个集合。例如:
给定元素14,可以得出:14 位于第一个集合。 给定元素65,可以得出:65 位于第二个集合。 给定元素20,可以得出:20 位于第五个集合。 并:将两个集合合并,例如将个位为偶数的集合合并的到第一个集合,个位为奇数的集合合并到第二个集合,结果如下: {14, 48, 20}, {35, 65, 87}
相信通过上面的表述,已经知道,并查集维护的是一堆集合。用什么样的数据结构表示并查集?
对于并查集,有两个信息是必须要知道的:
元素的值。 集合的标号。 一个元素必须仅存在于一个集合中,一个集合中可以有多个元素。
元素对集合来说,是多对一的关系。这么看来可以用一个健值对的结构来表示并查集(键是元素,值时所属集合)。
但是如果对元素本身没有特定要求的话,可以使用数组,这样速度更快,使用起来也更加简单:
{0}, {1}, {2}, {3}, {4}, {5} => [0,1,2,3,4,5]
{0,1,2}, {3,4,5} => [0,0,0,3,3,3] or [1,1,1,4,4,4] 在解释上面的数组表示方式之前,不知道有没有发现一个事实:
元素本身的值是固定不变的,但是元素所属的集合是可以变化的” 因此可以使用两个数组:
第一个数组保存所有元素 第二个数组使用数组的 下标 来代表数组一中元素,对应位置上 存放的值 表示元素所属的集合。 例如:{0,1,2}, {3,4,5} => [0,0,0,3,3,3]:第一个数组是0, 1, 2, 3, 4, 5,第二个数组是0, 0, 0, 3, 3, 3。第一个数组保存了所有元素,第二个数组保存了元素所属集合。
第二个数组中,第一个元素是0,含义是:第一个数组的第一个元素属于 0 号集合。 第二个数组中,第二个元素是0,含义是:第一个数组的第二个元素属于 0 号集合。 第二个数组中,第三个元素是0,含义是:第一个数组的第三个元素属于 0 号集合。 第二个数组中,第四个元素是3,含义是:第一个数组的第四个元素属于 3 号集合。 第二个数组中,第五个元素是3,含义是:第一个数组的第五个元素属于 3 号集合。 第二个数组中,第六个元素是3,含义是:第一个数组的第六个元素属于 3 号集合。 说完了集合的表示,来看看如何基于这种表示去实现 “并” 和 “查”,也就是集合的合并和元素的查找,这两个操作是相互影响的。合并其实就是改变第二个数组中存放的值,这个值表示的是第一个数组对应位置元素所在的集合。
上述实现并查集方法很直观。但是将连个集合合并的时候,需要修改其中一个集合中的所有元素对应的数组二中的值,有没有办法优化下呢?
这个问题的源是:第二个数组中保存的是第一个数组中各个元素所属集合,所以合并集合的时候,第二个数组中需要修改元素比较多。
可以为每个元素选出一个代表它的元素,数组二中存放代表元素
例如:{0,1,2}, {3,4,5} => [0,0,0,3,3,3]:第一个数组是0, 1, 2, 3, 4, 5,第二个数组是0, 0, 0, 3, 3, 3。第一个数组保存了所有元素,第二个数组保存了能代表该元素的元素。
第二个数组中,0号位置保存的是0,含义是:第一个数组的0号位置保存的元素和 第一个数组中的0号位置保存的元素属于同一个集合。
第二个数组中,1号位置保存的是0,含义是:第一个数组的1号位置保存的元素和 第一个数组中的0号位置保存的元素属于同一个集合。
第二个数组中,2号位置保存的是0,含义是:第一个数组的2号位置保存的元素和 第一个数组中的0号位置保存的元素属于同一个集合。
第二个数组中,3号位置保存的是3,含义是:第一个数组的3号位置保存的元素和 第一个数组中的3号位置保存的元素属于同一个集合。
第二个数组中,4号位置保存的是3,含义是:第一个数组的4号位置保存的元素和 第一个数组中的3号位置保存的元素属于同一个集合。
第二个数组中,5号位置保存的是3,含义是:第一个数组的5号位置保存的元素和 第一个数组中的3号位置保存的元素属于同一个集合。
这个时候,如果要合并两个集合,只需要修改代表元素即可。
例如:将{0,1,2}, {3,4,5} => [0,0,0,3,3,3]中的第二个集合合并到第一个集合中,。只需要修改第二个集合的代表元素集合,合并后为:{0,1,2,3,4,5} => [0,0,0,0,3,3]
这个时候,问:5这个元素位于哪个集合?查找过程如下:
在数组一中找到 5 这个元素的位置下标是:5 在第二个数组中查看下标5位置保存的元素是3。说明5这个元素和3这个元素在同一个集合。 在数组一中找到 3 这个元素的位置下标是:3。在第二个数组中查看下标3位置保存的元素是0。说明5这个元素和0这个元素在同一个集合。 在数组一中找到 0 这个元素的位置下标是:0。在第二个数组中查看下标0位置保存的元素是0,也就是找到了代表元素。得出结论5这个元素的代表元素是0,他们在同一个集合。 第二个数组保存代表元素,就能简化集合的合并。
总结:
用一个数组保存对应位置元素所属集合的代表元素。 AB两个集合合并:将B集合代表元素的代表元素设置为A集合的代表元素。 查找C元素属于哪个集合:找C元素的代表元素,如果不是他自己,就重复查找代表元素的代表元素,知道查找到一个元素的代表元素是它自己,C就属于整个代表元素所代表的集合。
代码实现:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int p[N];//存放代表元素
//查找 x 所属的集合,就是 x 元素的代表元素
int find(int x)
{
//如果 x 的代表元素不是他自己,就递归的x的代表元素修改为代表元素的代表元素
if(x != p[x]) p[x] = find(p[x]);
return p[x];
}
//合并a b所在的两个集合
void merge(int a, int b)
{
int pa = find(a);//找到 a 所在集合的代表元素
int pb = find(b);//找到 b 所在集合的代表元素
if(pa != pb)//如果不是同一个,则属于不同集合,需要合并
{
p[pa] = pb;//将a所在集合代表元素的代表元素设置为b所在集合的代表元素。
}
}
void query(int a,int b)
{
int pa = find(a);//找到 a 所在集合的代表元素
int pb = find(b);//找到 b 所在集合的代表元素
//判断 a 和 b 是否有同一个代表元素
if(pa == pb) cout << "Yes" << endl;
else cout << "No" << endl;
return ;
}
int main()
{
int n, m;
cin >> n >> m;
//初始化代表元素集合,开始的时候,各自属于一个集合,即每个元素的代表元素是他自己。
for (int i = 1; i <= n; i ++ )
p[i] = i;
while (m -- )
{
char op;
cin >> op;
int a, b;
cin >> a >> b;
if(op == 'M')
{//合并
merge(a, b);
}
else
{//查询
query(a, b);
}
}
}