算法与数据结构——并查集

在这里插入图片描述


一、概述

定义:
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。

主要构成:
并查集主要由一个整型数组pre[ ]和两个函数find( )、join( )构成。
数组 pre[ ] 记录了每个点的前驱节点是谁,函数 find(x) 用于查找指定节点 x 属于哪个集合,函数 join(x,y) 用于合并两个节点 x 和 y 。

作用:
并查集的主要作用是求连通分支数(如果一个图中所有点都存在可达关系(直接或间接相连),则此图的连通分支数为1;如果此图有两大子图各自全部可达,则此图的连通分支数为2……)


二.函数的定义与实现

1.find( )函数

故事引入:
子夜,小昭于骊山下快马送信,发现一头戴竹笠之人立于前方,其形似黑蝠,倒挂于树前,甚惧,正系拔剑之时,只听四周悠悠传来:“如此夜深,姑凉竟敢擅闯明教,何不下坐陪我喝上一盅?”。小昭听闻,后觉此人乃明教四大护法之一的青翼蝠王韦一笑,答道:“在下小昭,乃紫衫龙王之女”。蝠王轻惕,急问道:“尔等既为龙王之女,故同为明教中人。敢问阁下教主大名,若非本教中人,于明教之地肆意走动那可是死罪!”。小昭吓得赶紧打了个电话问龙王:“龙王啊,咱教主叫啥名字来着?”,龙王答道:“吾教主乃张无忌也!”,小昭遂答蝠王:“张无忌!”。蝠王听后,抱拳请礼以让之。
在上面的情境中,小昭向他的上级(紫衫龙王)请示教主名称,龙王在得到申请后也向他的上级(张无忌)请示教主名称,此时由于张无忌就是教主,因此他直接反馈给龙王教主名称是“张无忌”。同理,青翼蝠王也经历了这一请示过程。
在并查集中,用于查询各自的教主名字的函数就是我们的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;					//教主驾到~~~
}


2.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;                        //方丈只好委委屈屈地当了师太的手下啦
}


3.路径压缩算法之一(优化find( )函数)

我们可以通过递归的方法来逐层修改返回时的某个节点的直接前驱(即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 
}

该算法存在一个缺陷:只有当查找了某个节点的代表元(教主)后,才能对该查找路径上的各节点进行路径压缩。换言之,第一次执行查找操作的时候是实现没有压缩效果的,只有在之后才有效。


三.例题

1.题目描述

布置宴席最微妙的事情,就是给前来参宴的各位宾客安排座位。无论如何,总不能把两个死对头排到同一张宴会桌旁!这个艰巨任务现在就交给你,对任何一对客人,请编写程序告诉主人他们是否能被安排同席。

输入格式:
输入第一行给出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

本道题目中应用了上述方法,可以帮助大家更好的理解并查集


2.基本思路

朋友的朋友是朋友,敌人的敌人和朋友的敌人,敌人的朋友关系不确定,因此可以使用一维数组fir[ ]用并查集存放朋友之间的关系,用二维数组flat[ ][ ]单独存放敌人的关系


3.代码实现

#include <stdio.h>
#include <stdlib.h>

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;
} 

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值