一.定义
定义:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
主要构成:
并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。
作用:
并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)
二.代码
find( )函数的定义与实现
故事引入:
子夜,小昭于骊山下快马送信,发现一头戴竹笠之人立于前方,其形似黑蝠,倒挂于树前,甚惧,正系拔剑之时,只听四周悠悠传来:“如此夜深,姑凉竟敢擅闯明教,何不下坐陪我喝上一盅?”。小昭听闻,后觉此人乃明教四大护法之一的青翼蝠王韦一笑,答道:“在下小昭,乃紫衫龙王之女”。蝠王轻惕,急问道:“尔等既为龙王之女,故同为明教中人。敢问阁下教主大名,若非本教中人,于明教之地肆意走动那可是死罪!”。小昭吓得赶紧打了个电话问龙王:“龙王啊,咱教主叫啥名字来着?”,龙王答道:“吾教主乃张无忌也!”,小昭遂答蝠王:“张无忌!”。蝠王听后,抱拳请礼以让之。
在上面的情境中,小昭向他的上级(紫衫龙王)请示教主名称,龙王在得到申请后也向他的上级(张无忌)请示教主名称,此时由于张无忌就是教主,因此他直接反馈给龙王教主名称是“张无忌”。同理,青翼蝠王也经历了这一请示过程。
在并查集中,用于查询各自的教主名字的函数就是我们的find()函数。find(x)的作用是用于查找某个人所在门派的教主,换言之就是用于对某个给定的点x,返回其所属集合的代表。
实现:
首先我们需要定义一个数组:int pre[1000]; (数组长度依题意而定)。这个数组记录了每个人的上级是谁。这些人从0或1开始编号(依题意而定)。比如说pre[16]=6就表示16号的上级是6号。如果一个人的上级就是他自己,那说明他就是教主了,查找到此结束。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。
每个人都只认自己的上级。比如小昭只知道自己的上级是紫衫龙王。教主是谁?不认识!要想知道自己教主的名称,只能一级级查上去。因此你可以视find(x)这个函数就是找教主用的。
下面给出这个函数的具体实现:
int find(int x) //查找x的教主
{
while(pre[x] != x) //如果x的上级不是自己(则说明找到的人不是教主)
x = pre[x]; //x继续找他的上级,直到找到教主为止
return x; //教主驾到~~~
}
join( )函数的定义与实现
故事引入:
虚竹和周芷若是我个人非常喜欢的两个人物,他们的教主分别是玄慈方丈和灭绝师太,但是显然这两个人属于不同门派,但是我又不想看到他们打架。于是,我就去问了下玄慈和灭绝:“你看你们俩都没头发,要不就做朋友吧”。他们俩看在我的面子上同意了,这一同意非同小可,它直接换来了少林和峨眉的永世和平。
实现:
在上面的情景中,两个已存的不同门派就这样完成了合并。这么重大的变化,要如何实现?要改动多少地方?其实很简单,我对玄慈方丈说:“大师,麻烦你把你的上级改为灭绝师太吧。这样一来,两派原先所有人员的教主就都变成了师太,于是下面的人们也就不会打起来了!反正我们关心的只是连通性,门派内部的结构不要紧的”。玄慈听后立刻就不乐意了:“我靠,凭什么是我变成她手下呀,怎么不反过来?我抗议!”。抗议无效,我安排的,最大。反正谁加入谁效果是一样的,我就随手指定了一个,join()函数的作用就是用来实现这个的。
join(x,y)的执行逻辑如下:
1、寻找 x 的代表元(即教主);
2、寻找 y 的代表元(即教主);
3、如果 x 和 y 不相等,则随便选一个人作为另一个人的上级,如此一来就完成了 x 和 y 的合并。
下面给出这个函数的具体实现:
void join(int x,int y) //我想让虚竹和周芷若做朋友
{
int fx=find(x), fy=find(y); //虚竹的老大是玄慈,芷若MM的老大是灭绝
if(fx != fy) //玄慈和灭绝显然不是同一个人
pre[fx]=fy; //方丈只好委委屈屈地当了师太的手下啦
}
总结
1、用集合中的某个元素来代表这个集合,则该元素称为此集合的代表元;
2 、一个集合内的所有元素组织成以代表元为根的树形结构;
3 、对于每一个元素 x,pre[x] 存放 x 在树形结构中的父亲节点(如果 x 是根节点,则令pre[x] = x);
4 、对于查找操作,假设需要确定 x 所在的的集合,也就是确定集合的代表元。可以沿着pre[x]不断在树形结构中向上移动,直到到达根节点。
因此,基于这样的特性,并查集的主要用途有以下两点:
1、维护无向图的连通性(判断两个点是否在同一连通块内,或增加一条边后是否会产生环);
2、用在求解最小生成树的Kruskal算法里。
一般来说,一个并查集对应三个操作:
1、初始化( Init()函数 )
2、查找函数( Find()函数 )
3、合并集合函数( Join()函数 )
#include<bits/stdc++.h>
using namespace std;
#define int long long
int n,m;
int pre[10007];
void init(){
for(int i=1;i<=n;i++){
pre[i]=i;//每个结点的上级都是自己
}
}
int find(int x){ //查找结点 x的根结点
if(pre[x]!=x){
pre[x]=find(pre[x]);//递归出口:x的上级为 x本身,则 x为根结点
}
return pre[x];//递归查找
}
void join(int x,int y){
int fx=find(x);//寻找 x的代表元
int fy=find(y);//寻找 y的代表元
if(fx!=fy){//不同集合则合并
pre[fx]=fy;
}
}
bool isSame(int x,int y){
return find(x)==find(y);
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
init();
while(m--){
int num;
cin>>num;
if(num==1){
int x,y;
cin>>x>>y;
join(x,y);
}else{
int x,y;
cin>>x>>y;
if(isSame(x,y)==0){
cout<<"N"<<endl;
}else{
cout<<"Y"<<endl;
}
}
}
system("pause");
return 0;
}
三.例题一
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m,k;
int pre[10010];
void init(){
for(int i=1;i<=n;i++){
pre[i]=i;
}
}
int find(int x){
if(pre[x]!=x){
pre[x]=find(pre[x]);
}
return pre[x];
}
void join(int x,int y){
int fx=find(x);
int fy=find(y);
if(fx!=fy){
pre[fx]=fy;
}
}
bool issame(int x,int y){
return find(x)==find(y);
}
int main(){
cin>>n>>m>>k;
init();
while(m--){
int a,b;
cin>>a>>b;
join(a,b);
}
while(k--){
int a,b;
cin>>a>>b;
if(issame(a,b)){
cout<<"Yes"<<endl;
}else{
cout<<"No"<<endl;
}
}
system("pause");
return 0;
}
四.例题二 (加反集)
关于反集
我也是做这道题才知道的
如果a和b是敌人,合并n+b和a,n+a和b
如果c和a是敌人,合并n+c和a,n+a和c
那么b和c就并在一起了
这样就符合了题目敌人的敌人是朋友的规则
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,k;
int pre[10010];
map<int,int> mp;
void init(){
for(int i=1;i<=2*n;i++){
pre[i]=i;//这里初始化为2*n
}
}
int find(int x){
if(pre[x]!=x){
pre[x]=find(pre[x]);
}
return pre[x];
}
void join(int x,int y){
int fx=find(x);
int fy=find(y);
if(fx!=fy){
pre[fx]=fy;
}
}
int main(){
cin>>n>>k;
init();
while(k--){
string s;
cin>>s;
int a,b;
cin>>a>>b;
if(s=="F"){
join(a,b);
}else if(s=="E"){
join(n+a,b);
join(n+b,a);
}
}
int cnt=0;
for(int i=1;i<=n;i++){
if(pre[i]==i) cnt++;
}
cout<<cnt<<endl;
system("pause");
return 0;
}