并查集
一个很友好 的数据结构(bushi)
并查集:(union-find sets)是一种简单的用途广泛的集合.
并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多,如其求无向图的连通分量个数、最小公共祖先、带限制的作业排序,还有最完美的应用:实现Kruskar算法求最小生成树(以后会学到)。
并查集的两种基本操作:
- (1)合并;
- (2)查询;
解释:对并查集概念的理解说白了就是一种合并集合的一个“算法思想”和结构方法,这是说并查集是一种方法,一个思路,也是一种数据结构,算是树形数据结构。
一个小故事对并查集的两种操作的形象化解释:
故事背景:
7777年的7月7日,有77个人被邀请去参加一个大赛,大赛官方设定规则,要求这77个人自由组队,争夺一项宝物,刚开始每一个人都是独立的个体,与谁组队全凭自愿,所以刚开始每个人都各自是一个单独的团队。
(1)合并
有的势力力量过于强大,所以为了打败他们夺得宝物,有的团队就要进行结盟,合并团队。一个团队总要有一个首领,这个首领是每一个人的上级,如果把b和a合并,那么就要让a的所有人奉b的首领为首领,这样a和b两个团队的所有人的首领都是原来b的首领,这样就形成了一个新的团队了。这就是集合的合并。即a的首领的首领变为b的首领。
变为
(2)查询
现在有两个团队正在交战,为了争夺宝物,但是由于团队人数都特别多,有的人不能判断眼前这个人到底和自己是不是一个团队的,所以开始疑惑到底要不要“暴揍”这个人,为此,他们想出了一个点子,问一问对方的最大的首领是谁,如果发现对方最大的首领一样,那皆大欢喜,咱俩原来是一伙的!那就一块揍别人去;若果发现并不是一伙的,那就开始“干架”(bushi)
并查集的两个操作大致如此~上案例
例题:洛谷P1551亲戚题目
大致意思,现在给出若干个点,并且已知部分点具有相连关系(我们假设具有相连关系的一些点处于一个集合之中),要通过一系列操作进行集合的查找或者集合的合并,并得出结果。算法核心思路:
——路径压缩,化多层树形结构为单层树形结构。
初始化: 每一个结点的上级都是自己,也就是说独立成一个集合 合并集合操作:
void merge(int x,int y)
{
f[find(y)]=find(x);//让y的首领成为x的首领的小弟qwq
}
解释: f[i]指的是结点i的上一级结点的序号。
代码含义–>将y的上一级变为x的上一级的下级,也就是说让y的上一级成为x的同级(即y是x的下一级关系)
抛去上下级关系来看,y的最高级和x的最高级是同一个结点,这个样子可以知道,x和y算是一个集合之中。
查找操作:(含有路径压缩)
int find(int x)
{
if(f[x]==x)
return x;//说明这个x就是首领
else
return f[x]=find(f[x]);//路径压缩一波,让x的上一级首领变为x的现在上一级首领的上一级首领。
//递归一下就成了x首领就是根节点首领了
}
注重解释路径压缩的部分(利用函数递归的思想,也就是栈结构) 假设现在已知结点1的上一级是结点3,节点3的上一级是结点5;即1->3->5 结点2的上一级是结点4;即2->4 现在我们想知道1和2是不是同一个集合关系。那么我们就要找1和2的最高级是不是一个结点对不对?也就是if(find(1)==find(2)) 那么就是同一个集合,反之就不是。
解释代码:
我们找find(1),很明显f[1]==3!=1,故find(1)返回f[1]=findf[1]这个时候我们注意到返回的不是一个值,是又一个函数,我们就需要进入到返回的那个函数find(3)去,一直直到返回是一个具体的值为止。
那么现在find(1)没有执行完毕,函数递归了,我们把find(1)压入栈中。现在开始执行find(3).很显然f[3]也是不等于3,应该返回f[3]=find(f[3])也就是返回f[3]=find(5);同样的,find(3)没执行完,压入栈中。接着执行find(5).这个时候好了,f[5]==5。那正好返回5。现在该把栈给弹出来了,最上面的是find(3),find(3)返回值是find(5)==5。也就是返回find(3)==5;接着弹出最后一个find(1).返回f[1]=find(3)==5。这样一个查找过程就完成了。所以我们知道结点1 的最高级是结点5。同样操作知道结点2的最高级是结点4,显然不在一个集合里面。
补充说明:在查找的过程中进行了路径压缩,所以这个过程不仅仅是得到了查找的内容,同时还优化改变了内容。仔细看,在这一个查找过程中得到了结点1的最高级是结点5,但是同时也使f[1]=5。这样直接把1的上级变成了5.在下一次操作如果还要搜索1的最高级是谁,那么直接可以知道f[1]==5,就不用再次重复上述那个递归函数的麻烦操作了,极大地简化了代码的复杂度。
图示:
附上代码:
#include<iostream>
using namespace std;
const int maxn = 1000000;
int f[maxn];
int find(int x)
{
if(f[x]==x)
return x;//说明这个x就是首领
else
return f[x]=find(f[x]);//路径压缩一波,让x的上一级首领变为x的现在上一级首领的上一级首领。
//递归一下就成了x首领就是根节点首领了
}·
void merge(int x,int y)
{
f[find(y)]=find(x);//让y的首领称为x的首领的小弟qwq
}
int main()
{
int n,m,k;
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
f[i]=i;
for(int i=1;i<=m;i++)
{
int a,b;
cin>>a>>b;
merge(a,b);
}
for(int i=1;i<=k;i++)
{
int a,b;
cin>>a>>b;
if(find(a)==find(b))
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}