并查集+路径压缩

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

并查集两个操作“并”和“查”:
并:将两个集合合并
查:询问两个数是否在一个集合中 

(1)初始化。定义数组int s[]是以结点i为元素的并查集,开始的时候,还没有处理点与点之间的朋友关系,所以每个点属于独立的集,并且以元素i的值表示它的集s[i],例如元素1的集s[1]=1。
  下面是图解,左边给出了元素与集合的值,右边画出了逻辑关系。为了便于讲解,左边区分了结点i和集s:把集的编号加上了下划线;右边用圆圈表示集,方块表示元素。

 图1 并查集的初始化

 (2)合并,例如加入第一个朋友关系(1, 2)。在并查集s中,把结点1合并到结点2,也就是把结点1的集1改成结点2的集2。

图2 合并(1, 2) 

(3)合并,加入第二个朋友关系(1, 3)。查找结点1的集,是2,再递归查找元素2的集是2,然后把元素2的集2合并到结点3的集3。此时,结点1、2、3都属于一个集。右图中,为简化图示,把元素2和集2画在了一起。

图3 合并(1, 3)

(4)合并,加入第三个朋友关系(2, 4)。结果如下,请读者自己分析。

图4 合并(2, 4) 

(5)查找。上面步骤中已经有查找操作。查找元素的集,是一个递归的过程,直到元素的值和它的集相等,就找到了根结点的集。从上面的图中可以看到,这棵搜索树的高度,可能很大,复杂度是O(n)的,变成了一个链表,出现了树的“退化”现象。 

(6)统计有多少个集。如果s[i] = i,这是一个根结点,是它所在的集的代表;统计根结点的数量,就是集的数量。

具体代码 

find()函数

int find(int x){               //查找
    return x==s[x]? x:find(s[x]);
}

路径压缩算法:优化find( )函数

这里并没有明确谁是谁的前驱的规则,而是我直接指定后面的数据作为前面数据的前驱。那么这样就导致了最终的树状结构无法预计,即有可能是良好的 n 叉树,也有可能是单支树结构(一字长蛇形)。试想,如果最后真的形成单支树结构,那么它的效率就会及其低下(树的深度过深,那么查询过程就必然耗时)。
而我们最理想的情况就是所有人的直接上级都是教主,这样一来整个树的结构就只有两级,此时查询教主只需要一次。因此,这就产生了路径压缩算法。
实现:
从上面的查询过程中不难看出,当从某个元素出发去寻找它的代表元时,我们会途径一系列的元素,在这些节点中,除了代表元外,其余所有节点都需要更改直接前驱为曹操。
因此,基于这样的思路,我们可以通过递归的方法来逐层修改返回时的某个节点的直接前驱(即pre[x]的值)。简单说来就是将x到根节点路径上的所有点的pre(上级)都设为根节点。下面给出具体的实现代码:

int find(int x)     				//查找结点 x的根结点 
{
    if(s[x] == x) return x;		//递归出口:x的上级为 x本身,即 x为根结点        
    return s[x] = find(s[x]);	//此代码相当于先找到根结点 rootx,然后s[x]=rootx 
}

join函数

join函数的作用是将两个集合合并,合并两个集合的关键是找到两个集合的代表元,将其中一个作为另一个的前驱结点即可。

void join(int x,int y)                     
{
    int fx=find(x), fy=find(y); 
    if(fx != fy) 
        s[fx]=fy;	//fy做fx的前驱结点
}

附上一道模板题

P3367 【模板】并查集

链接:https://www.luogu.com.cn/problem/P3367

题目描述

如题,现在有一个并查集,你需要完成合并和查询操作。

输入格式

第一行包含两个整数 N,MN,M ,表示共有 NN 个元素和 MM 个操作。

接下来 MM 行,每行包含三个整数 Z_i,X_i,Y_iZi​,Xi​,Yi​ 。

当 Z_i=1Zi​=1 时,将 X_iXi​ 与 Y_iYi​ 所在的集合合并。

当 Z_i=2Zi​=2 时,输出 X_iXi​ 与 Y_iYi​ 是否在同一集合内,是的输出 Y ;否则输出 N 。

输出格式

对于每一个 Z_i=2Zi​=2 的操作,都有一行输出,每行包含一个大写字母,为 Y 或者 N 。

输入输出样例

输入 #1

4 7
2 1 2
1 1 2
2 1 2
1 3 4
2 1 4
1 2 3
2 1 4

输出 #1

N
Y
N
Y

说明/提示

对于 30\%30% 的数据,N \le 10N≤10,M \le 20M≤20。

对于 70\%70% 的数据,N \le 100N≤100,M \le 10^3M≤103。

对于 100\%100% 的数据,1\le N \le 10^41≤N≤104,1\le M \le 2\times 10^51≤M≤2×105,1 \le X_i, Y_i \le N1≤Xi​,Yi​≤N,Z_i \in \{ 1, 2 \}Zi​∈{1,2}。

 AC代码

#include <bits/stdc++.h>
using namespace std;
int n,m,z,x,y,s[10005];//s[i]是第i号节点的祖先
int find(int x) {
	if (x==s[x]) return x;
	return s[x]=find(s[x]);
	//路径压缩,可以缩短时间
}
void join(int x,int y) {
	int fx=find(x), fy=find(y);
	if(fx != fy)
		s[fx]=fy;	//fy做fx的前驱结点
}
int main() {
	cin>>n>>m;
	for(int i=1; i<=n; ++i) {
		s[i]=i;
		//并查集的初始化
	}
	while(m--) {
		cin>>z>>x>>y;
		if(z==1) {
			join(x,y);
		} else {
			if(find(x)==find(y))
				printf("Y\n");
			else
				printf("N\n");
		}
	}
	return 0;
}

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值