并查集(Disjoint Set Union,DSU)详解
由DS倾情奉献,由CJ详细修改
并查集:
对集合进行合并merge、查找find两个操作
f[i]:i所在的集合代表是谁(i所在学校的校长是谁/i的上级/i的爹/i的父亲/i的祖先)
初始化f[i] = i : 每个人是自己的上级
合并merge(x,y):合并x和y所在的集合,可能是x合并到y,也可能是y合并到x
俗称:x认y为爹,或y认x为爹
查询find(x):查询x的上级是谁
朴素查询:逐级往上找上级,好处是可以清晰的知道层级关系
坏处:慢,O(N)
路径压缩:通过一次查询,改变f[i]等于整个集合的祖先(最高上级)
好处是,第一次O(N),接下来都是O(1)
坏处是,只知道祖先是谁,不知道爹是谁,忘本
扩展题:权值并查集:在忘本之前维护一些数据
性质:执行完n个点,m条边的并查集后
可以通过f[i] == i的条件数量判断n个点被区分成了几个集合
一、基本概念
并查集是一种树型数据结构,用于处理不相交集合的合并与查询问题。它支持以下两种操作:
-
Find:查询元素所属集合 -
Union:合并两个集合
核心思想
通过维护一个父节点数组,使得:
-
同一集合的元素最终指向同一个根节点
-
不同集合的元素的根节点不同
二、基础实现
1. 初始化
2. 查找(Find)
// 普通查找(无路径压缩) int find(int x) { while(f[x] != x) x = f[x]; return x; } // 带路径压缩的查找 int find(int x) { if(f[x] != x) f[x] = find(f[x]); // 路径压缩 return f[x]; }
3. 合并(Union)
void merge(int x, int y) { int fx = find(x), fy = find(y); if(fx != fy) f[fy] = fx; // 将fy的父节点设为fx }
三、优化技巧
1. 路径压缩
在查找时将节点直接指向根节点,使树更扁平
2. 按秩合并
int rank[N]; // 秩数组 void merge(int x, int y) { int fx = find(x), fy = find(y); if(fx != fy) { if(rank[fx] > rank[fy]) f[fy] = fx; else { f[fx] = fy; if(rank[fx] == rank[fy]) rank[fy]++; } } }
四、扩展应用
1. 带权并查集
维护节点到父节点的距离信息
int f[N], d[N]; // d[i]表示i到f[i]的距离 int find(int x) { if(f[x] != x) { int root = find(f[x]); d[x] += d[f[x]]; // 维护距离 f[x] = root; } return f[x]; }
2. 种类并查集(扩展域)
通过扩大域表示多种关系
if(find(x) != find(y)){ //x和y是敌人 merge(x,y + n); //x和y的敌人(y+n)是朋友 merge(y,x + n); //y和x的敌人(x+n)是朋友 }
五、典型例题解析
例题1:银河英雄传说(带权并查集)
问题:维护战舰队列,支持合并和查询两战舰间距离
解决方案:
int f[N], size[N], d[N]; // size记录集合大小,d记录到父节点距离 int find(int x) { if(f[x] != x) { int root = find(f[x]); d[x] += d[f[x]]; // 维护距离 f[x] = root; } return f[x]; } void merge(int x, int y) { int fx = find(x), fy = find(y); if(fx != fy) { f[fx] = fy; d[fx] = size[fy]; // 新距离为fy集合大小 size[fy] += size[fx]; } }
例题2:关押罪犯(种类并查集)
问题:将罪犯分到两个监狱,使最大冲突最小
解决方案:
for(int i = 1; i <= 2 * n; i++) f[i] = i; //1~n敌人,n+1~2n朋友 for(int i = 1; i <= m; i++) { int x = t[i].x, y = t[i].y; if(find(x) == find(y)) { // 冲突不可避免 cout << t[i].z; return 0; } merge(x, y + n); // x和y必须分开 merge(y, x + n); }
-
1~n:表示罪犯在监狱A的状态 -
n+1~2n:表示同一个罪犯在监狱B的"镜像状态"(不是简单的朋友/敌人关系)
六、复杂度分析
| 操作 | 普通实现 | 路径压缩 | 路径压缩+按秩合并 |
|---|---|---|---|
| Find | O(n) | O(α(n)) | O(α(n)) |
| Union | O(n) | O(α(n)) | O(α(n)) |
其中α(n)是反阿克曼函数,在可预见的n范围内不超过4
七、应用场景
-
连通性问题
-
图的动态连通性
-
分组问题
-
最近公共祖先(离线算法)
-
关系推理(敌对、友好等)
八、模板代码
#include<bits/stdc++.h> using namespace std; const int N = 1e5 + 10; // 定义最大节点数量 int f[N]; // 并查集父节点数组 // 初始化并查集 void init(int n) { for(int i = 1; i <= n; i++) { f[i] = i; // 初始时每个节点都是自己的父节点 } } // 查找操作(带路径压缩优化) int find(int x) { if(f[x] != x) // 如果x不是根节点 f[x] = find(f[x]); // 路径压缩:将x直接指向根节点 return f[x]; // 返回根节点 } // 合并操作 void merge(int x, int y) { int fx = find(x); // 找到x的根节点 int fy = find(y); // 找到y的根节点 if(fx != fy) { // 如果不在同一个集合 f[fx] = fy; // 将fx的父节点设为fy } } /* * 基础并查集实现说明: * 1. 只有路径压缩优化,没有按秩合并 * 2. 适用于大多数不需要严格平衡树的场景 * 3. 均摊时间复杂度接近O(1) */
九、练习题推荐
-
P3367 【模板】并查集
-
P1551 亲戚
-
P1196 [NOI2002]银河英雄传说
-
P1525 [NOIP2010]关押罪犯
-
P2024 [NOI2001]食物链
掌握并查集的关键在于理解其树型结构和路径压缩原理,并熟练应用带权和种类扩展等高级技巧。

浙公网安备 33010602011771号