并查集详解

引入

例题:(洛谷 P3367 【模板】并查集

第一行包含两个整数 N , M N,M N,M ,表示共有 N N N 个元素和 M M M 个操作。
接下来 M M M 行,每行包含三个整数 Z i , X i , Y i Z_i,X_i,Y_i Zi,Xi,Yi
Z i = 1 Z_i=1 Zi=1 时,将 X i X_i Xi Y i Y_i Yi 所在的集合合并。
Z i = 2 Z_i=2 Zi=2 时,输出 X i X_i Xi Y i Y_i Yi 是否在同一集合内,是的输出 Y;否则输出 N

显然可以暴力解决。。。

请添加图片描述
这就要用到一种数据结构——并查集了!

并查集

并查集可以看做是一种森林,初始时每个节点都指向它自己。

我们来打一个合适的比喻:现在有四位武林高手,它们各自创建了自己的门派,开始都只有他们自己一人:

请添加图片描述
现在1号大师和2号大师打了一架,2号大师输了,加入了1号大师的门派:

请添加图片描述
3号大师和4号大师打了一架,4号大师输了,加入了3号大师的门派:

请添加图片描述
这时,4号(已经不能称之为大师了)去找2号打了一架,徒弟惹了事自然要师傅担着了,所以3号大师率领整个门派加入了1号大师:

请添加图片描述
这就是并查集的建立过程,那如果我们想知道一位好汉是哪一门派的怎么办?当然是找他师傅,通过他师父找他师傅的师傅,再找他师傅的师傅的师傅。。。一直找到掌门人。

用代码怎么实现呢?

int root(int x)
{
	if(x==f[x])//如果他是掌门人
		return x;//他所在的门派就是他自己的
	return root(f[x]);//找师傅
}

那么帮派之间的合并又是怎样完成的呢?自然是找到两位好汉所在门派的掌门人,然后让一个掌门人加入另一个掌门人:

void join(int x,int y)
{
	int r1=root(x),r2=root(y);//找掌门人
	if(r1!=r2)//如果它们不是一个门派
		f[r1]=r2;//打不过就加入
}

并查集的优化——路径压缩

假设我们有这样一个门派:
请添加图片描述

那这样不行啊,下达命令太慢,友军还没等到救援就已经被灭了。。。

那应该怎么办呢?采用 中央集权统治 路径压缩!

路径压缩后的并查集变成了这样:

请添加图片描述
这么神奇的功能是怎么实现的呢?

int root(int x)
{
	if(x==f[x])
		return x;
	f[x]=root(f[x]);//路径压缩,直属中央管辖
	return f[x];
}

注意!路径压缩会破坏树形结构!在题目不需要维护树形结构时才可使用。

并查集的优化——按秩合并

假设有两个帮派为了对付共同的敌人,同仇敌忾,想要友好建交,将两个帮派合并,这时它们的合并不在乎谁是老大,只要合并就行。

那么,我们应该让哪个帮派并入哪个帮派更好呢?显然,将更小的帮派并入更大的帮派更好,这样省事省力。

事实上,在并查集中,一般并不需要维护两个节点的先后关系,而只关心两个节点的所在集合。使用按秩合并,可以减少查询的时间。

按秩合并分为两种:按点的数量合并和按树的高度合并。这里采用树的高度。

void join(int x,int y)
{
	int r1=root(x),r2=root(y);
	if(r1!=r2)
	{
		if(h[r1]<h[r2])
			f[r1]=r2;
		else
		{
			f[r2]=r1;
			if(h[r1]==h[r2])
				h[r1]++;//增加树高
		}
	}
}

注意,使用前将 h [ ] h[] h[]数组初始化为 1 1 1

为什么两个树高度相同,合并时就要加 1 1 1呢?我们来看以下例子:

请添加图片描述
它们高度相同,假设我们将节点 4 4 4加到节点 1 1 1

请添加图片描述
显然,树高加 1 1 1

并查集的终极优化——路径压缩&按秩合并

我们要想达到真正飞一般的速度,我们要既使用路径压缩,又使用按秩合并

代码:(其实就是放到一起)

int root(int x)
{
	if(x==f[x])
		return x;
	f[x]=root(f[x]);
	return f[x];
}
void join(int x,int y)
{
	int r1=root(x),r2=root(y);
	if(r1!=r2)
	{
		if(h[r1]<h[r2])
			f[r1]=r2;
		else
		{
			f[r2]=r1;
			if(h[r1]==h[r2])
				h[r1]++;
		}
	}
}

这个方法有多快呢?它的复杂度为 O ( α ( n ) ) O(\alpha(n)) O(α(n)),其中 α ( n ) \alpha(n) α(n)是阿克曼函数的反函数。

不清楚?这么说吧,百度百科对 α ( n ) \alpha(n) α(n)函数是这么说的:

因为Ackermann函数的增长很快,所以其反函数 α ( x ) α(x) α(x)的增长是非常慢的,对所有在实际问题中有意义的 x x x α ( x ) ≤ 4 α(x)≤4 α(x)4,所以在算法时间复杂度分析等问题中,可以把 α ( x ) α(x) α(x)看成常数。

事实上,当
x ≤ 2 2 1 0 19279 x≤2^{2^{10^{19279}}} x221019279
时, α ( x ) ≤ 5 α(x)≤5 α(x)5

完整代码

#include<iostream>
#include<cstdio>
#define MAXN 10010
using namespace std;
int n,m;
int f[MAXN];
int h[MAXN];
int root(int x)
{
	if(x==f[x])
		return x;
	f[x]=root(f[x]);
	return f[x];
}
void join(int x,int y)
{
	int r1=root(x),r2=root(y);
	if(r1!=r2)
	{
		if(h[r1]<h[r2])
			f[r1]=r2;
		else
		{
			f[r2]=r1;
			if(h[r1]==h[r2])
				h[r1]++;
		}
	}
}
int op,x,y;
void init()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		f[i]=i,h[i]=1;
}
int main()
{
	init();
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&op,&x,&y);
		if(op==1)
			join(x,y);
		else
			if(root(x)==root(y))
				printf("Y\n");
			else
				printf("N\n");
	}
	return 0;
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值