2021年hznu寒假集训第二天 并查集

2021年hznu寒假集训第二天

关键词:代表元
解决问题:问题的朴素解法
概念:并查集主要记录节点之间的链接关系,而没有其他的具体的信息,仅仅代表某个节点与其父节点之间存在联系,它多用来判断图的连通性,如下图所示,这是一个并查集,其中箭头表示父子关系。
在这里插入图片描述

优化

路径压缩

我们通常把路径优化后的结果叫做菊花图.
即将上面的图变成下面的
在这里插入图片描述
接下来我们来介绍一下路径压缩.
C的父节点是B,而B的父节点是A,所以我们就可以把A设为C的父节点。
接下来看一下代码:

int find(int x)
{
	return x==parent[x]?parent[x]:parent[x]=find(parent[x]);
}

合并

在这里插入图片描述
比如这题,我们想要证明环存在,进行完路径压缩,我们要合并。
合并就是将两个集变成一个集
在这里插入图片描述

如图所示,0、2的父节点是1, 3的父节点是4,要想这两个集有关联,我们要尝试将他们的父节点相关联,例如这样
在这里插入图片描述
那这个过程我们就把他叫做合并。
下面是它的代码:

void Union(int x,int y){
	int fx=find(x),fy=find(y);
	//找出父节点
	if(fx!=fy)
	//证明父节点不相同,即他们是在两个集合里面
		parent[fx]=fy;
		//则将其中一个父节点y设为另一个父节点x的父节点
}

下面我们来解决一下上面证明环存在的问题

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

#define VERTICES 6
void initialise(int parent[]){
	int i;
	for(i=0;i<VERTICES;i++){
		parent[i]=-1;
	}
}
int find_root(int x,int parent[]){
	int x_root=x;
	while(parent[x_root]!=-1){
		x_root=parent[x_root];
	}
	return x_root;
}
/*1- union successfully,0-failed*/
int union_vertices(int x,int y,int parent[]){
	int x_root=find_root(x,parent);
	int y_root=find_root(y,parent);
	if(x_root==y_root){
		return 0;
	}
	else{
		parent[x_root]=y_root;
		return 1;
	}
}
int main(){
	int parent[VERTICES]={0};
	int edges[6][2]={
		{0,1},{1,2},{1,3},
		{2,4},{3,4},{2,5}
	};
	initialise(parent);
	int i;
	for(i=0;i<6;i++){
		int x=edge[i][0];
		int y=edge[i][1];
		if(union_vertices(x,y,parent)==0){
			printf("Cycle detected!\n");
			exit(0);
		}
	}
	printf("NO cycles found.\n");
	return 0;
}

但是这串代码有一个缺陷,如果我们的图是
比如第一次union(0,1),第二次(1,2),第三次(2,3)……
这个时候,会形成一个特别长的链,在查找parent的时候就会执行特别多次,时间复杂度是log n级别的
所以我们这个时候要再进行优化。

按秩合并

首先我们要引入秩的概念。
rank:指代树的高度
在这里插入图片描述
这棵树x的高度是3;
在这里插入图片描述
而这棵树y的高度则是1;
如果我们把树y接在树x上,即x作为y的parent,那么,他的rank并不发生改变;
反过来,他的rank变成了4;
后者显然是我们不希望的,所以我们要按秩合并。

if rank[x]>rank[y]
	parent[y]=x;
	//rank 不变->x
else
	parent[x]=y;

之前那道题的代码就可以优化了

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

#define VERTICES 6
void initialise(int parent[],int rank[]){
	int i;
	for(i=0;i<VERTICES;i++){
		parent[i]=-1;
		rank[i]=0;
	}
}
int find_root(int x,int parent[]){
	int x_root=x;
	while(parent[x_root]!=-1){
		x_root=parent[x_root];
	}
	return x_root;
}
/*1- union successfully,0-failed*/
int union_vertices(int x,int y,int parent[],int rank[]){
	int x_root=find_root(x,parent);
	int y_root=find_root(y,parent);
	if(x_root==y_root){
		return 0;
	}
	else{
		//parent[x_root]=y_root;
		if(rank[x_root]>rank[y_root]){
			parent[y_root]=x_root;
		}
		else{
			parent[x_root]=y_root;
		}
		return 1;
	}
}
int main(){
	int parent[VERTICES]={0};
	int rank[VERTICES]={0};
	int edges[6][2]={
		{0,1},{1,2},{1,3},
		{2,4},{3,4},{2,5}
	};
	initialise(parent,rank);
	int i;
	for(i=0;i<6;i++){
		int x=edge[i][0];
		int y=edge[i][1];
		if(union_vertices(x,y,parent,rank)==0){
			printf("Cycle detected!\n");
			exit(0);
		}
	}
	printf("NO cycles found.\n");
	return 0;
}

应用

最小生成树的构成

维护无向图连通性

拓展

带权并查集

种族并查集

种族并查集,就是把同一个集合中的每个元素赋予多个不同的属性,在不同属性的对应元素间建立关系,关系与关系之间能够虽然有冗余,但是互相联系能够很快得出两个元素之间的关系。

经典题目:
团伙
关押罪犯
食物链
三者做法基本相同,取其中食物链讲一下。

常规法:
1.p[x]表示x根结点。r[x]表示p[x]与x关系。r[x]=0 表示p[x]与x同类;1表示p[x]吃x;2表示x吃p[x]。
2.怎样划分一个集合呢?
注意,这里不是根据x与p[x]是否是同类来划分。而是根据“x与p[x]能否确定两者之间关系”来划分,若能确定x与p[x]关系,则它们同属一个集合
3.3.怎样判断一句话是不是假话?
假设已读入D ,X ,Y ,先利用findset()函数得到X,Y所在集合代表元素fx,fy,若它们在同一集合(即fx=fy)则可以判断这句话真伪:
若 D=1 而 r[X]!=r[Y] 则此话为假.(D=1 表示X与Y为同类,而从r[X]!=r[Y]可以推出 X 与 Y 不同类.矛盾.)
若 D=2 而 r[X]=r[Y](X与Y为同类)或者r[X]=(r[Y]+1)%3(Y吃X)则此话为假。
4.上个问题中r[X]=(r[Y]+1)%3这个式子怎样推来?
假设有Y吃X,那么r[X]和r[Y]值是怎样?
  我们来列举一下:
r[X]=0&&r[Y]=2
r[X]=1&&r[Y]=0
r[X]=2&&r[Y]=1
稍微观察一下就知道r[X]=(r[Y]+1)%3;
事实上,对于上个问题有更一般判断方法:
若(r[Y]-r[X]+3)%3!=D-1 ,则此话为假.
5.其他注意事项:
在Union(d,x,y)过程中若将S(fy)合并到S(fx)上,则相应r[fy]必须更新为fy相对于fx关系。怎样得到更新关系式?
r[fy]=(r[x]-r[y]+d+3)%3;


#include<cstdio>
const int N=50001;
int p[N],r[N],n;
int findset(int x)
{
	if(x!=p[x])
	{
		int fx=findset(p[x]);
		r[x]=(r[x]+r[p[x]])%3;
		p[x]=fx;
	}
	return p[x];
}
bool Union(int d,int x,int y)
{
	int fx=findset(x),fy=findset(y);
	if(fx==fy)
	{
		if((r[y]-r[x]+3)%3!=d)return 1;
		else return 0;
	}
	p[fy]=fx;
	r[fy]=(r[x]-r[y]+d+3)%3;
	return 0;
}
int main()
{
	int k,ans,i,d,x,y;
	scanf("%d%d",&n,&k);
	ans=0;
	for(i=1;i<=n;i++)p[i]=i,r[i]=0;
	while(k--)
	{
		scanf("%d%d%d",&d,&x,&y);
		if(x>n||y>n||(x==y&&d==2)){ans++;continue;}
		if(Union(d-1,x,y))ans++;
	}
	printf("%d\n",ans);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值