前言:并查集(Disjoint Set Union, DSU),作为高阶的数据结构,专门解决“动态连通性”问题。
比如:
• 社交网络:判断两个人是否是好友?
• 迷宫逃脱:起点和终点是否连通?
• 电路连接:两个触电是否通电?
本文专注于介绍算法竞赛中的并查集的创建以及使用。
一、📌并查集的实现
并查集本质上使用双亲表示法实现的森林。
1.1 双亲表示法
双亲表示法是一种用于树结构存储的方法。简单来说,就是在存储树中的每个节点时,除了存储节点本身的数据外,还存储该节点的双亲节点(即父节点)的位置信息。
特别的,我们实现并查集的时候一般让根结点自己指向自己。
1.2 并查集的核心操作
核心功能:
✅ 合并(Union):连接两个元素,注意这里连接的是两个集合。
✅ 查询(Find):判断两个元素是否属于同一集合。一般会在每个集合中选择一个代表元素,查询的是这个代表元素。
🔥 并查集的灵魂:代表元法
每个集合选一“老大”,判断元素是否同属一个集合,只需看它们的老大是否相同
1.2.1 初始化(Init)
初始状态下所有的元素单独作为一个集合,让元素自己指向自己即可。
for(int i = 1; i <= n; i++) fa[i] = i;
1.2.2 查找(Find)
我们前边所说查找操作时查找的每个集合的代表元素,只要一直向上找爸爸即可~
int find(int x)
{
if(x == fa[x]) return x;
else return find(fa[x]);
// 一句话搞定~
//return x == fa[x] ? x : find(fa[x]);
}
1.2.3 合并(Union)
把两个元素所在的集合合并在一起,就是让其中一个元素的根结点指向另一个元素的根结点。
void un(int x, int y) // 不要起union
{
int fx = find(x);
int fy = find(y);
// 修改指向
fa[fx] = fy;
}
1.2.4 判断(issame)
判断两个元素是否在同一个集合,看看他俩的大哥是否相同即可~
bool issame(int x, int y)
{
int fx = find(x);
int fy = find(y);
return fx == fy;
}
1.2.5 全部测试代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int fa[N];
int n;
int find(int x)
{
if(x == fa[x]) return x;
// 路径压缩
return fa[x] = find(fa[x]);
//return x == fa[x] ? x : fa[x] = find(fa[x]);
}
void un(int x, int y) // 不要起union
{
int fx = find(x);
int fy = find(y);
// 修改指向
fa[fx] = fy;
}
bool issame(int x, int y)
{
int fx = find(x);
int fy = find(y);
return fx == fy;
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) fa[i] = i;
return 0;
}
二、💡 并查集的优化
最坏情况:在合并的过程中,整棵树会退化成一个链表。
核心操作的时间复杂度会退化成O(n)。
路径压缩:在查询时,把被查询的节点到根节点路径上所有的节点的父节点设置为根节点,从而减少树的深度。也就是说在向上查询的同时,把路径上的每个节点都直接连在根节点的后面。
int find(int x)
{
if(x == fa[x]) return x;
// 路径压缩
return fa[x] = find(fa[x]);
//return x == fa[x] ? x : fa[x] = find(fa[x]);
}
tip:还有一种优化方式是按秩优化,在算法竞赛中基本用不到,并查集的时间复杂度已经很优秀了,大家做个了结就可。
三、⚡ 并查集的时间复杂度
操作 | 无优化 | 带路径压缩 & 按秩合并 |
---|---|---|
Find/Union | O(n) | 几乎 O(1)
O(α(n))
|
四、🧠扩展域并查集
4.1 概念
扩展域并查集是通过将原问题中的元素域进行扩展,从而把一些原本复杂的关系问题转化为并查集可以处理的简单集合合并及查询问题。例如,在一些涉及到元素之间多种关系的场景中,我们可以为每种关系创建一个对应的 “域”。
它可以处理简单并查集无法处理的多关系问题。
1.2 实现思路
假设有n个元素,我们通常会将并查集数组扩展为3n(这里以常见的三种关系为例)。其中,1-n表示元素本身的集合,n + 1到2n表示元素的关系集合,2n + 1到3n表示另一种关系集合(当然如果还有其他关系可以继续扩展),顺带提一嘴:扩展的方法也常用于处理环形问题。当我们在进行合并操作的时候,就不能只关心元素集合的合并,还要根据题目意思合并关系集合。
操作类型 | 合并方式 | 关系推导 |
---|---|---|
结盟 | union(x_self, y_self) | x与y是盟友 |
敌对 | union(x_self, y_enemy) | x的敌人是y,y的敌人是x的盟友 |
union(x_enemy, y_self) |
五、🧠带权并查集
5.1 概念
带权并查集通常也用于处理多关系集合问题,为每个结点增加一个权值,我们要赋予这个权值一个意义,然后用这些结点的距离来表示关系。通过维护和更新这些权值,我们可以解决一些需要知道元素间具体关系距离或状态的问题。
5.2 实现思路
在带权并查集的实现中,除了常规的父节点指针外,还需要一个数组来记录每个节点的权值。在合并操作时,不仅要合并两个集合,还要根据两个集合根节点的权值关系来更新新的根节点权值以及路径压缩过程中涉及节点的权值。在查找操作的路径压缩过程中,同时更新节点的权值,使得权值能够正确反映节点与新根节点的关系。
也就是说在普通并查集的基础上进行find和union操作时,不仅要维护集合结构还要维护结点的权值。
5.3 实例
这里的权值表示的是距离根结点的距离。
5.3.1 初始化(init)
for(int i = 1; i <= n; i++)
{
fa[i] = i;
d[i] = 0;// 根据题目
}
5.3.2 查找(find)
int find(int x)
{
if(x == fa[x]) return x;
// 记录根结点,路径压缩
int t = find(fa[x]);
// 修改距离根结点的长度
d[x] += d[fa[x]];
return fa[x] = t;
}
5.3.3 合并(union)
// w代表的是x与y之间的权值
void un(int x, int y, int w)
{
int fx = find(x), fy = find(y);
if(fx != fy)
{
fa[fx] = fy;
d[fx] = d[y] + w - d[x];
}
}
5.3.4 查询(query)
int query(int x, int y)
{
int fx = find(x), fy = find(y);
if(fx != fy) return -1; // 不在一个集合
return d[y] - d[x];
}
5.3.5 测试代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int fa[N], d[N];
int find(int x)
{
if(x == fa[x]) return x;
// 记录根结点,路径压缩
int t = find(fa[x]);
// 修改距离根结点的长度
d[x] += d[fa[x]];
return fa[x] = t;
}
// w代表的是x与y之间的权值
void un(int x, int y, int w)
{
int fx = find(x), fy = find(y);
if(fx != fy)
{
fa[fx] = fy;
d[fx] = d[y] + w - d[x];
}
}
int query(int x, int y)
{
int fx = find(x), fy = find(y);
if(fx != fy) return -1; // 不在一个集合
return d[y] - d[x];
}
int main()
{
// 初始化
for(int i = 1; i <= n; i++)
{
fa[i] = i;
d[i] = 0;// 根据题目
}
return 0;
}
四、📚 初学必刷题
简单并查集:
扩展域并查集:
带权并查集:
题目的代码,请移步下一篇博客!!!