定义:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
主要构成:
并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。
作用:
并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)
故事引入:
子夜,小昭于骊山下快马送信,发现一头戴竹笠之人立于前方,其形似黑蝠,倒挂于树前,甚惧,正系拔剑之时,只听四周悠悠传来:“如此夜深,姑凉竟敢擅闯明教,何不下坐陪我喝上一盅?”。小昭听闻,后觉此人乃明教四大护法之一的青翼蝠王韦一笑,答道:“在下小昭,乃紫衫龙王之女”。蝠王轻惕,急问道:“尔等既为龙王之女,故同为明教中人。敢问阁下教主大名,若非本教中人,于明教之地肆意走动那可是死罪!”。小昭吓得赶紧打了个电话问龙王:“龙王啊,咱教主叫啥名字来着?”,龙王答道:“吾教主乃张无忌也!”,小昭遂答蝠王:“张无忌!”。蝠王听后,抱拳请礼以让之。
在上面的情境中,小昭向他的上级(紫衫龙王)请示教主名称,龙王在得到申请后也向他的上级(张无忌)请示教主名称,此时由于张无忌就是教主,因此他直接反馈给龙王教主名称是“张无忌”。同理,青翼蝠王也经历了这一请示过程。
在并查集中,用于查询各自的教主名字的函数就是我们的find()函数。find(x)的作用是用于查找某个人所在门派的教主,换言之就是用于对某个给定的点x,返回其所属集合的代表。
实现:
首先我们需要定义一个数组:int pre[1000]; (数组长度依题意而定)。这个数组记录了每个人的上级是谁。这些人从0或1开始编号(依题意而定)。比如说pre[16]=6就表示16号的上级是6号。如果一个人的上级就是他自己,那说明他就是教主了,查找到此结束。也有孤家寡人自成一派的,比如欧阳锋,那么他的上级就是他自己。
int find(int x) //查找x的教主
{
while(pre[x] != x) //如果x的上级不是自己(则说明找到的人不是教主)
x = pre[x]; //x继续找他的上级,直到找到教主为止
return x; //教主驾到~~~
}
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; //方丈只好委委屈屈地当了师太的手下啦
}
我们可以通过递归的方法来逐层修改返回时的某个节点的直接前驱(即pre[x]的值)。简单说来就是将x到根节点路径上的所有点的pre(上级)都设为根节点。
int find(int x) //查找结点 x的根结点
{
if(pre[x] == x) return x; //递归出口:x的上级为 x本身,即 x为根结点
return pre[x] = find(pre[x]); //此代码相当于先找到根结点 rootx,然后pre[x]=rootx
}
该算法存在一个缺陷:只有当查找了某个节点的代表元(教主)后,才能对该查找路径上的各节点进行路径压缩。换言之,第一次执行查找操作的时候是实现没有压缩效果的,只有在之后才有效。
布置宴席最微妙的事情,就是给前来参宴的各位宾客安排座位。无论如何,总不能把两个死对头排到同一张宴会桌旁!这个艰巨任务现在就交给你,对任何一对客人,请编写程序告诉主人他们是否能被安排同席。
输入格式:
输入第一行给出3个正整数:N(≤100),即前来参宴的宾客总人数,则这些人从1到N编号;M为已知两两宾客之间的关系数;K为查询的条数。随后M行,每行给出一对宾客之间的关系,格式为:宾客1 宾客2 关系,其中关系为1表示是朋友,-1表示是死对头。注意两个人不可能既是朋友又是敌人。最后K行,每行给出一对需要查询的宾客编号。
这里假设朋友的朋友也是朋友。但敌人的敌人并不一定就是朋友,朋友的敌人也不一定是敌人。只有单纯直接的敌对关系才是绝对不能同席的。
输出格式:
对每个查询输出一行结果:如果两位宾客之间是朋友,且没有敌对关系,则输出No problem;如果他们之间并不是朋友,但也不敌对,则输出OK;如果他们之间有敌对,然而也有共同的朋友,则输出OK but…;如果他们之间只有敌对关系,则输出No way。
输入样例:
7 8 4
5 6 1
2 7 -1
1 3 1
3 4 1
6 7 -1
1 2 1
1 4 1
2 3 -1
3 4
5 7
2 3
7 2
输出样例:
No problem
OK
OK but…
No way
本道题目中应用了上述方法,可以帮助大家更好的理解并查集
朋友的朋友是朋友,敌人的敌人和朋友的敌人,敌人的朋友关系不确定,因此可以使用一维数组fir[ ]用并查集存放朋友之间的关系,用二维数组flat[ ][ ]单独存放敌人的关系
#include
#include
int fir[105]={0}; //用做合并是否为朋友
int flat[105][105]={0}; //标记是否为敌人,1为敌人
int find(int x){ //用做对应头朋友的标号
if(fir[x]==x)
return x;
else
return fir[x]=find(fir[x]); //压缩路径
}
int merge(int a,int b){ //合并新的朋友关系
int b1=find(a); //获取a对应的头朋友
int b2=find(b);
if(b1!=b2) //如果不相等,说明两个还没有确定朋友关系
fir[b2]=b1;
}
int main(int argc,char *argv[]){
//argc是主程序参数个数,也就是出入参数的个数,这个数一开始是不确定的,要根据自己输入了多少参数而确定
//argv[]数组存入的是传入参数的个数
int n,m,k,i,a,b,c;
scanf("%d %d %d",&n,&m,&k);
for(i=1;i<=n;i++){
fir[i]=i; //标上标号
}
for(i=0;i<m;i++){
scanf("%d %d %d",&a,&b,&c);
if(c==1){ //关系为朋友
merge(a,b);
}else{
flat[a][b]=1; //充分考虑到两种情况
flat[b][a]=1;
}
}
int b1,b2;
for(i=0;i<k;i++){
scanf("%d %d",&a,&b);
b1=find(a);
b2=find(b);
if(b1==b2&&flat[a][b]==0){ //两位宾客之间是朋友,且没有敌对关系
printf("No problem\n");
}else if(b1!=b2&&flat[a][b]==0){ //他们之间并不是朋友,但也不敌对
printf("OK\n");
}else if(b1==b2&&flat[a][b]==1){ //他们之间有敌对,然而也有共同的朋友
printf("OK but...\n");
}else{ //他们之间只有敌对关系
printf("No way\n");
}
}
return 0;
}