并查集(Union-Find Disjoint Sets/DSU)

并查集是一种树状数据结构,用于处理不交集查询问题。文章通过实例介绍了并查集的概念,包括初始化、查询和合并过程,并提供了相关代码示例。此外,还讲解了三道与并查集相关的编程题目,帮助读者理解和运用并查集解决问题。
摘要由CSDN通过智能技术生成

目录

 一:明确概念

1.什么是并查集

2.并查集相关过程

3.并查集相关代码

初始化部分代码

查询部分代码

合并部分代码

二:题目讲解

P3367 【模板】并查集

P1536 村村通

P3958 [NOIP2017 提高组] 奶酪


本文题目传送门:

【模板】并查集 - 洛谷

村村通 - 洛谷

[NOIP2017 提高组] 奶酪 - 洛谷


 一:明确概念

1.什么是并查集

        并(unity)查(find)集(set),即可以合并、查找的树状数据结构,常用于处理不交集查询问题。

        下面是一个典型例子:

图1

        图1中每个箭头两端连接的是两名呈同班同学关系的学生,请问,1号同学和5号同学是同一个班的吗?

        我们简化一下,就是如果我们能确定任意两个人之间是不是同班同学,我们能不能确定所有同学之间是不是同班同学。

        在简化,在同一个班的学生里,指定一个老大(t),当查询两个学生(x,y)时,只需要看者两个学生是否与老大间接认识或直接认识,如果都认识,那么者两个学生就在同一个班里。

2.并查集相关过程

        给出条件: 

1号与2号是同学

3号与4号是同学

5号与2号是同学

4号与6号是同学

2号与6号是同学

8号与7号是同学

9号与7号是同学

1号与6号是同学

2号与4号是同学

        最开始,所有人的同学都是自己(似乎没毛病)。数组father代表的是每一位同学箭头所指

的同学,也就是与其呈直接认识关系的另一位同学(有点绕)。

  

         下面,我们要让1和2建立同学关系,让2当1的老大。

        2已经成功当上了1的老大,我们让4也称为3的老大。

         下面我们让6把4和他的小弟收编了,让2成为5的老大。

 

         现在6又把2和他的小弟收编了。

         下面8和9投靠了7。

 

         下面1和6建立直接认识关系,2和4页建立直接认识关系。

        这就是并查集的基本过程,下面是代码部分。

3.并查集相关代码

        下面是并查集通用代码。

初始化部分代码
void init(int n)
{
	for(int i=1; i<=n; ++i)
	{
		father[i]=i;
		//最开始,每个人只和自己是直接认识的,也就是说自己是自己的老大
	}
}
查询部分代码
int find(int x)
{
	//如果x是自己的同伴,那x就是这个队伍的老大,返回其本身即可。
	if(father[x]==x)
	{
		return x;
	}
	//否则就继续寻找x的老大father[x],看father[x]的老大是谁
	else
	{
		return find(father[x]);
	}
}
合并部分代码
//合并x和y所在的队伍
int combine(int x,int y)
{
	//p为x所在队伍的老大
	//q为y所在队伍的老大
	int p=find(x);
	int q=find(y);
	if(p==q)
	{
		//两者在同一个队伍中
		return 0;
	}
	//将x所在的队伍p合并到q的队伍中
	else
	{
		father[p]=q;
		return 1;
	}
}

二:题目讲解

题目链接在最前面        

P3367 【模板】并查集

        这道题说白了就是要判断两个学生的老大是不是同一个人,其实很简单,只需要用到这个

if(find(x)==find(y))

        但是注意,一个人只能有一个老大,所以当有a已经拥有老大s,而来做b的小弟时,s成为b的小弟。

        上代码!

#include <bits/stdc++.h>
using namespace std;
int n,m,father[10005];
int find(int x)//这个函数前面说到过
{
	if(father[x]==x) return x;
	return father[x]=find(father[x]);
}
void combine(int x,int y)//这个也一样
{
	int fax=find(x),fay=find(y);
	if(fax!=fay)
	{
		father[fax]=fay;
	}
}
int main()
{
	cin>>n>>m;
	for(int i=1; i<=n; i++)
	{
		father[i]=i;
	}
	for(int i=1; i<=m; i++)
	{
		int x,y,z;
		cin>>z>>x>>y;
		if(z==1)
		{
			combine(x,y);
		}
		else
		{
			if(find(x)==find(y))
			{
				cout<<"Y"<<endl;
			}
			else
			{
				cout<<"N"<<endl;
			}
		}
	}
	return 0;
}

        小试牛刀一下,很简单对吧QWQ

P1536 村村通

        这道题,我们没输入两个村庄,就把它们连起来。如果遇到没有老大的村庄(祖先是没有老大的),就是遇到了一个需要建设的村庄。

        注意:最终答案要-1

        猜猜为啥?三个点,做成连通图,要几条线?两条对不对。四个点就要三条线,所以最终答案-1。

        上代码!

#include <bits/stdc++.h>
using namespace std;
int fa[1000001], n, m, x, y;
int find(int x)
{
    if(x!=fa[x])//当x不等于它的老大时(当它是祖先时,它没有老大) 
    {
        fa[x]=find(fa[x]);//继续找他的老大的老大
    }
    return fa[x];//返回祖先 
}
void combine(int x, int y)
{
    int r1=find(x);//寻找祖先 
    int r2=find(y); 
    fa[r1]=r2;//祖先和祖先结为老大(谁是老大谁是儿子都可以) 
}
int main()
{
	while(1)
	{
		int ans=0;
		cin>>n;
		if(n==0)
		{
			return 0;
		}
		cin>>m;
	    for(int i=1; i<=n; i++)//初始化
	    {
	        fa[i]=i;
	    }
	    for(int i=1; i<=m; i++)
	    {
	        cin>>x>>y;
	        combine(x, y);
	    }
	    for(int i=1; i<=n; i++)
	    {
	    	if(find(i)==i)//自己的老大就是自己本身
	    	{
	    		ans++;
			}
		}
		printf("%d\n", ans - 1);
	}
    return 0;
}
P3958 [NOIP2017 提高组] 奶酪

         这道题思路很简单。两个洞如果相交或相切,就编入同一个队伍中,一个队伍就是一个通道。只需要判断每个通道是否存在元素与底部、顶部连接即可。

        那怎么判断两个圆是否相交呢?如果两个圆半径之和大于或等于两个圆圆心的距离,那么两个圆相交。

        上代码!

#include<bits/stdc++.h>
using namespace std;
int father[1001];
int find(int x)
{
	if (x!=father[x]) father[x]=find(father[x]);
	return father[x];
}
long long dis(long long x,long long y,long long z,long long x1,long long y1,long long z1)
{
	return (x-x1)*(x-x1)+(y-y1)*(y-y1)+(z-z1)*(z-z1);
}//两点距离公式
long long x[100001],y[100001],z[100001];
int f1[100001],f2[100001];//f1记录与顶面相交的洞的序号,f2记录与底面相交的洞的序号
int main(){
	int t;
	cin>>t;
	int n,h; 
	long long r;
	for(int i=1;i<=t;i++)
	{
		cin>>n>>h>>r;
		int tot1=0;//记录与顶面相交的洞有几个
		int tot2=0;//记录与底面相交的洞有几个
		for(int j=1;j<=n;j++)
		{
			father[j]=j;  //并查集初始化
		}
		for (int j=1;j<=n;j++)
		{
			cin>>x[j]>>y[j]>>z[j];
			if(z[j]+r>=h)
			{
				tot1++;
				f1[tot1]=j;
			}
			if(z[j]-r<=0)
			{
				tot2++;
				f2[tot2]=j;
			}
			for (int k=1;k<=j;k++)//枚举之前的洞是否与这个洞相交,如果相交则合并集合
			{
				if ((x[j]-x[k])*(x[j]-x[k])+(y[j]-y[k])*(y[j]-y[k])>4*r*r) continue;//防止爆long long的特判。 
				if (dis(x[j],y[j],z[j],x[k],y[k],z[k])<=4*r*r)
				{
					int a1=find(j);
					int a2=find(k);
					if (a1!=a2) father[a1]=a2;
				}
			}
		}
		int s=0;
		//看看每一个中是否有洞连接上下面
		for(int j=1;j<=tot1;j++)
		{
			for(int k=1;k<=tot2;k++)
			{
				if(find(f1[j])==find(f2[k]))
				{
					s=1; 
					break;
				}
			}
			if(s==1) break;
		}
		if(s==1) cout<<"Yes"<<endl;
		else cout<<"No"<<endl;
	}
	return 0;
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值