并查集学习笔记

Part 1     Introduction / 并查集简介 \text{Part 1\,\,\,Introduction / 并查集简介} Part 1Introduction / 并查集简介

并查集(Disjoint Set),是一种图论里的数据结构。并查集,顾名思义,可以完成"并"和"查"两种操作。"并"指的是合并两个集合,"查"指的是查询元素所在的集合。

Part 2     Example / 例题引入 \text{Part 2\,\,\,Example / 例题引入} Part 2Example / 例题引入

题目链接:亲戚

给定 n n n 个人和 m m m 对关系,其中一对关系用 M i , M j M_i,M_j Mi,Mj 来表示,指的是 M i M_i Mi M j M_j Mj 是亲戚。亲戚的亲戚也是亲戚。 后有 p p p 次询问,每一次询问 P i P_i Pi P j P_j Pj 是否为亲戚。 n , m , p ≤ 5000 n,m,p \le 5000 n,m,p5000

分析:

本题是一道经典的并查集题目。对于每一组亲戚(组内所有人均互相是亲戚关系),我们可以将其看作一棵树,并为其指定一个根。那么,对于两个有亲戚关系的人,它们所在的树必然相等,那么根也就相等。

想到这里我们自然的产生了一种想法:对于第 i i i 个人,我们用 f a i fa_i fai 记录这个人所在树的根,那么查询也就好办了:对于两个人 i , j i,j i,j,只需判断 f a i fa_i fai f a j fa_j faj 是否相等即可。

对于给定的亲戚操作,如何处理?观察可以发现,当给出 x x x y y y 是亲戚后, x x x 所在的树和 y y y 所在的树内所有元素均互相具有亲戚关系。那么这两棵树相当于合并成为了一棵树,由于树只能有 1 1 1 个根,所以我们选择其中一个根,将其作为另一个根的儿子即可。在合并的过程中,我们需要更新 f a fa fa 数组。

以上这个数据结构,我们就将其称为:并查集。

Part 3     Implementation / 代码如何实现? \text{Part 3\,\,\,Implementation / 代码如何实现?} Part 3Implementation / 代码如何实现?

我们已经有了初步的想法,那么如何实现?又如何在合并过程中维护 f a fa fa 数组?

对于这个问题,我们需要实现一个核心函数: find(x) ⁡ \operatorname{find(x)} find(x)。这个函数的作用就是找到 x x x 所在的树的根。这个函数我们可以用递归来实现,伪代码如下:

int find(int x) { //查找树的根
    if(x 为当前树的根) 返回 x;
    else 返回 find(x 的父亲);
}

将其写成 C++ \texttt{C++} C++ 代码就是:

int find(int x) {
	if(fa[x] == x) return x;
	return find(fa[x]);
}

有了这个函数,那么另外两个操作都好办了:对于合并操作 merge \text{merge} merge,查找两棵树的根,并将一个树的根作为另一棵树的根的儿子;对于查询操作 ask \text{ask} ask,判断两棵树根是否相等即可。

Part 4     Std / 示范代码 \text{Part 4\,\,\,Std / 示范代码} Part 4Std / 示范代码

以下为根据以上讲解,写出的 P1551 的完整代码。

注意:在初始化时,每个元素都是单独的一棵树,需要将它的根设为自己。

#include <bits/stdc++.h>
using namespace std;
int fa[100005];
int getfa(int x) { //重点 find() 函数
	if(fa[x] == x) return x;
	return getfa(fa[x]);
}
void merge(int x,int y) { //合并
	if(getfa(x) == getfa(y)) return ;
	fa[getfa(x)] = getfa(y);
}
bool issamefa(int x,int y) { //查询
	return getfa(x) == getfa(y);
} 
int main() {
	int n,m,q; cin >> n >> m >> q;
	for(int i = 1;i <= n;i++) fa[i] = i; //初始化
	for(int i = 1;i <= m;i++) {
		int x,y; cin >> x >> y;
		merge(x,y);
	}
	while(q--) {
	    int x,y; cin >> x >> y;
	    cout << (issamefa(x,y) ? "Yes" : "No") << endl;
	}
	return 0;
}

Part 5     Optimize / 优化手段 \text{Part 5\,\,\,Optimize / 优化手段} Part 5Optimize / 优化手段

Part 5.1     Path compression / 路径压缩 \text{Part 5.1\,\,\,Path compression / 路径压缩} Part 5.1Path compression / 路径压缩

路径压缩是一种常见的并查集优化手段。

考虑如下两张图:

对于元素 2 , 3 , 4 , 5 2,3,4,5 2,3,4,5,这两张图是没有变化的。但是,在第一张图中,查询 5 5 5 的根需要递归 3 3 3 层,而在第二张图中只需要 1 1 1 层。由此可见,第二张图在效率上优于第一张图。

那么如何实现呢?很简单:只需将 return find(fa[x]) 改为 return fa[x] = find(fa[x]) 即可。

Q:为什么要这样写呢?

A:因为相当于将自己的父亲直接设为了整棵树的根,自然就达到了我们的目标。

n n n 个结点的树里面,按照原来的方式查询,平均复杂度为 O ( log ⁡ n ) O(\log n) O(logn) ;最坏情况下,整棵树为一条链,此时复杂度退化为 O ( n ) O(n) O(n)。按照优化后的方式查询,只需 O ( 1 ) O(1) O(1) 的复杂度。当 n = 1 0 5 n=10^5 n=105 或更大时,这是一个很有效的优化。

*Part 5.2     Merge by rank / 按秩合并 \text{*Part 5.2\,\,\,Merge by rank / 按秩合并} *Part 5.2Merge by rank / 按秩合并

按秩合并是一种在合并时的优化。

这种优化在合并时,会将深度更小的那一棵树合并到更大的那一棵树上,这样不仅能降低时间复杂度,还能尽量减少树的深度。

Part 6     Summarize / 总结 \text{Part 6\,\,\,Summarize / 总结} Part 6Summarize / 总结

并查集是一种基本操作较为简单,应用广泛的数据结构,使用它可以解决许多与集合合并等相关的问题。

Part 7 Practice / 习题 \text{Part 7 Practice / 习题} Part 7 Practice / 习题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值