【高级数据结构】并查集

一、并查集的介绍

并查集 D i s j o i n t S e t ) Disjoint Set) DisjointSet)是一种精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。经典的应用有连通图,最小生成树 K r u s k a l Kruskal Kruskal算法、最近公共祖先 ( L e a s t C o m m o n A n c e s t o r s , L C A ) (Least CommonAncestors,LCA) (LeastCommonAncestors,LCA)等。并查集在算法竞赛中极为常见。

通常用“帮派”的例子说明并查集的应用场景。一个城市中有n个人,他们分成不同的帮派。同属于一个帮派的人相互之间是朋友,朋友的朋友是朋友。给出一-些人的关系,如 1 1 1号和 2 2 2号是朋友,1号和3号也是朋友,那么他们都属于一个帮派。在分析完所有朋友关系之后,问有多少帮派,每人属于哪个帮派。给出的 n = 10 n=10 n=10

读者可以先思考暴力法以及复杂度。如果用并查集实现,不仅代码很简单,而且查询的复杂度小于 O ( l o g 2 n ) O(log_2n) O(log2n)

并查集的概念:将编号分别为 1 − n 1-n 1n n n n个对象划分为不相交集合,在每个集合中,选择其中某个元素代表所在集合。

本节全面介绍并查集的基本操作,并查集的合并优化,并查集的查询优化——路径压缩和带权并查集。

并查集的基本应用是集合问题。在加上权值之后,利用并查集的合并优化和路径压缩,可以对权值所代表的具体应用进行高效的操作。

二、并查集的基本操作

并查集的基本操作有初始化、合并、查找,统计,下面举例说明。

1.初始化

定义数组 s [ ] s[] s[], s [ i ] s[i] s[i]门是元素i所属的并查集,开始时,还没有处理点与点之间的朋友关系,所以每个点属于独立的集,直接以元素i的值表示它的集 s [ i ] s[i] s[i]门,如元素 1 1 1的集 s [ 1 ] = 1 s[1]=1 s[1]=1

如图 4.1 4.1 4.1所示,下图给出了元素与集合的值,右图画出了逻辑关系。为了便于讲解,左图区分了节点主和集 s s s ,把集的编号加上了下画线﹔右图用圆圈表示集,用方块表示元素。
在这里插入图片描述

2.合并

( 1 ) (1) (1)加入第1个朋友关系 ( 1 , 2 ) (1,2) (1,2)。在并查集s中,把节点 1 1 1合并到节点 2 2 2,也就是把节点 1 1 1的集 1 1 1.改为节点 2 2 2的集 2 2 2,如图 4.2 4.2 4.2所示。
在这里插入图片描述
( 2 ) (2) (2) 加入第 2 2 2个朋友关系 ( 1 , 3 ) (1,3) (1,3)。查找节点 1 1 1的集 2 2 2,再递归查找节点 2 2 2的集2然后把节点 2 2 2的集 ② ② 合并到节点 3 3 3的集 3 3 3。此时,节点 1 、 2 、 3 1、2、3 123都属于一个集。如图 4.3 4.3 4.3所示,为简化图示,把节点 2 2 2和集 2 2 2画在了一起。
在这里插入图片描述
( 3 ) (3) (3)加入第 3 3 3个朋友关系 ( 2 , 4 ) (2,4) (2,4)。结果如图 4.4 4.4 4.4所示,请读者自己分析。
在这里插入图片描述

3.查找

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

4.统计

如果 s [ i ] = i s[i]=i s[i]=i,这是一个根节点,是它所在的集的代表;统计根节点的数量,就是集的数量。

三、并查集的优化(路径压缩)

在上面的查询函数中,查询元素主所属的集需要搜索路径找到根节点,返回的结果是根节点。这条搜索路径可能很长。如果在返回时顺便把 i i i所属的集改为根节点,那么下次再搜时,就能在 O ( 1 ) O(1) O(1)的时间内得到结果。原理如图 4.5 4.5 4.5所示。

在这里插入图片描述

四、带权并查集

1.带权并查集的介绍

前面讲解了并查集的基本应用—一处理集合问题。在这些基本应用中,点之间只有简单的归属关系,而没有权值。如果在点之间加上权值,并查集的应用会更广泛。

如果读者联想到树这种数据结构,会发现并查集实际上是在维护若干棵树。并查集的合并和查询优化,实际上是在改变树的形状,把原来“细长”的,操作低效的大量“小树”,变为“粗短”的、操作高效的少量“大树”。如果在原来的“小树”上,点之间有权值,那么经过并查集的优化变成“大树”后,这些权值的操作也变得高效了。

定义一个权值数组 d [ ] d[] d[],把节点 i i i到父节点的权值记为 d [ i ] d[i] d[i]。下面介绍带权并查集的操作。

2.带权并查集的路径压缩

4.6 4.6 4.6所示为带权值的路径压缩。原来的权值 d [ i ] d[i] d[i],经过压缩之后,更新为 d [ i ] ′ d[i]' d[i],如 d [ 1 ] ′ = d [ 1 ] + d [ 2 ] + d [ 3 ] d[1]'=d[1]+d[2]+d[3] d[1]=d[1]+d[2]+d[3]

需要注意的是,这个例子中,权值是相加的关系,比较简单

在具体的题目的中,可能有相乘、异或等符合题意的操作。
在这里插入图片描述

3.带权并查集的合并

在合并操作中,把点 x x x与点 y y y合并,就是把点 x x x 的根节点 f x fx fx合并到点 y y y的根节点 f y fy fy。在 f x fx fx f y fy fy之间增加权值,这个权值要符合题目的要求。

四、题目推荐

1. 亲戚(并查集查询)

解题思路

直接使用并查集维护每个集合,如果二者在同一个集合就输出 Y e s Yes Yes,否则输出 N o No No

代码

//solve函数
const int N = 5e3 + 10;
int fa[N];
int n, m, p;
void init() {
	for (int i = 1; i <= n; ++i) fa[i] = i;
}
int find(int x) {
	if (x != fa[x]) {
		fa[x] = find(fa[x]);
	}
	return fa[x];
}
void solve() {
	cin >> n >> m >> p;
	init();
	for (int i = 1; i <= m; ++i) {
		int x, y;
		cin >> x >> y;
		int px = find(x), py = find(y);
		if (px != py) fa[px] = py;
	}
	while (p--) {
		int x, y;
		cin >> x >> y;
		if (find(x) == find(y)) cout << "Yes\n";
		else cout << "No\n";
	}
}
//main 函数 
signed main() {
	std::ios::sync_with_stdio(0);
	std::cout.tie(0);
	std::cin.tie(0);
	int t = 1;
	//cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

2.村村通(并查集统计)

解题思路

使用并查集维护每个集合,最后只要统计有多少个不同的集合即可,答案为不同集合的个数 − 1 -1 1

代码

//solve
const int N = 1e3 + 10;
int fa[N];
void init(int n) {
	for (int i = 1; i <= n; ++i) {
		fa[i] = i;
	}
}
int find(int x) {
	if (x != fa[x]) {
		fa[x] = find(fa[x]);
	}
	return fa[x];
}
void solve() {
	while (1) {
		int n, m;
		cin >> n;
		if (n == 0)break;
		cin >> m;
		init(n);
		for (int i = 1; i <= m; ++i) {
			int x, y;
			cin >> x >> y;
			int px = find(x), py = find(y);
			fa[px] = py;
		}
		set<int>st;
		for (int i = 1; i <= n; ++i) {
			st.insert(find(i));
		}
		cout << st.size() - 1 << "\n";
	}
}
//main 函数 
signed main() {
	std::ios::sync_with_stdio(0);
	std::cout.tie(0);
	std::cin.tie(0);
	int t = 1;
	//cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}

3. 银河英雄传说(带权并查集)

思路

使用带权并查集来维护每一个数到其根节点的权值,然后还要维护每一列的权值,表示其集合大小。每次合并集合的时候,在集合的根节点上加上移动的列的大小,然后在路径压缩的时候压缩权值,就可以进行O(1)查询每个点到其根结点的权值了。

代码

//solve函数
const int N = 5e5 + 10;
int fa[N];
int d[N],num[N];
int n;
void init() {
	for (int i = 1; i <= n; ++i) {
		fa[i] = i;
		num[i] = 1;
	}
}
int find(int x) {
	if (x != fa[x]) {
		int t = fa[x];
		fa[x] = find(fa[x]);
		d[x] += d[t];
	}
	return fa[x];
}
void solve() {
	cin >> n;
	init();
	while (n--) {
		char ch;
		int x, y;
		cin >> ch >> x >> y;
		if (ch == 'M') {
			int px = find(x);
			int py = find(y);
			fa[px] = py;
			
			d[px] += num[py];
			num[py] += num[px]; //维护每一列的个数
			num[px] = 0;

		}
		else {
			if (find(x) != find(y)) {
				cout << -1 << "\n";
			}
			else {
				cout << abs(d[x] - d[y])-1 << "\n";
			}
		}
	}
}
//main 函数 
signed main() {
	std::ios::sync_with_stdio(0);
	std::cout.tie(0);
	std::cin.tie(0);
	int t = 1;
	//cin >> t;
	while (t--) {
		solve();
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值