【Algorithm】Union-Find简单介绍
文章目录
- Union-Find
- 1 基本概念
- 1.1 `Find(x)` - 查询操作
- 1.2 `Union(x, y)` - 合并操作
- 2 并查集的结构和优化
- 2.1 数据结构设计
- 2.2 两大优化策略(关键)
- 2.2.1 路径压缩(Path Compression)
- 2.2.2 按秩合并(Union by Rank or Size)
- 3 使用并查集的注意事项
- 4 典型应用场景
- 4.1 判断连通性
- 4.2 等价类/合并集合
- 4.3 检测环路(图中是否有环)
- 4.4 岛屿问题/连通区域
- 4.5 网络连接问题
- 5 实现模板
- 6 问题示例:合并等价字符集合(字典序最小)
- 7 总结
Union-Find
1 基本概念
并查集是一种用于处理集合合并与查询的数据结构,主要用于解决:
- 判断两个元素是否属于同一个集合
- 合并两个集合
- 图的连通性问题(如 Kruskal 最小生成树、岛屿数量、等价类问题)
核心思想:每个元素初始是一个独立的集合(自成一个树的根)。使用两个操作:
- find(x):查找元素 x 所在集合的代表元(根)
- union(x, y) / unite(x, y):将元素 x 和 y 所在的两个集合合并为一个
1.1 Find(x)
- 查询操作
- 找出元素
x
所在集合的代表元(也叫根节点、父节点) - 判断两个元素是否属于同一个集合:只需比较它们的代表元是否相同
1.2 Union(x, y)
- 合并操作
- 将元素
x
和y
所在的两个集合合并 - 目的是把两个集合的元素归于同一个集合(也就是连通)
并查集的本质:将多个不相交的集合合并,并在查询时保持高效
2 并查集的结构和优化
2.1 数据结构设计
-
parent[i]
:表示第i
个元素的父节点(初始时每个元素是自己的父亲) -
常见扩展字段:
rank[i]
:节点的秩(可以理解为树的高度或大小,用于优化合并)size[i]
:集合的大小(如果你需要追踪每个集合的元素个数)
2.2 两大优化策略(关键)
2.2.1 路径压缩(Path Compression)
- 优化 find(x) 操作
- 将 x 到根节点路径上的所有节点直接指向根,降低后续查找的复杂度
- 时间复杂度近似 O(α(n)),α 是反阿克曼函数,几乎是常数
int find(int x) {if (parent[x] != x)parent[x] = find(parent[x]);return parent[x];
}
- 每次查询时,将
x
的所有祖先直接挂到根节点,形成扁平结构 - 减少下次查找路径长度
2.2.2 按秩合并(Union by Rank or Size)
- 合并时,总是将“较矮”的树合并到“较高”的树,保持整体平衡,防止链式退化
void union(int x, int y) {int px = find(x), py = find(y);if (px == py) return;if (rank[px] < rank[py])parent[px] = py;else if (rank[px] > rank[py])parent[py] = px;else {parent[py] = px;rank[px]++;}
}
3 使用并查集的注意事项
注意事项 | 说明 |
---|---|
初始化 | 每个元素一开始是自己的父节点(parent[i] = i ) |
找代表元要用 find() | 不要直接比较 parent[x] == parent[y] ,必须比较 find(x) == find(y) |
使用路径压缩 | 提高查找效率,避免变成链表结构 |
合并要检查代表元 | 避免重复合并或死循环 |
不适合有环结构查询 | 并查集不能表示通用图(除非用于检测是否成环) |
不支持高频动态插入删除 | 并查集适合处理固定集合或批量问题,不适合频繁插入删除 |
4 典型应用场景
并查集广泛应用于以下场景:
4.1 判断连通性
- 无向图中判断两个点是否连通
- 例题:
LeetCode 547. 省份数量
(朋友圈)
4.2 等价类/合并集合
- 把多个元素按关系合并为“集合组”
- 例题:
LeetCode 1061. 按字典序排列的最小等价字符串
4.3 检测环路(图中是否有环)
- 并查集用于无向图的成环检测
- 例题:
Kruskal 最小生成树
(MST)
4.4 岛屿问题/连通区域
- 将二维网格中相邻的“陆地”用并查集合并,统计岛屿数
- 例题:
LeetCode 200. 岛屿数量
4.5 网络连接问题
- 网络中节点是否连通、连接多少次才能连通
- 例题:
LeetCode 1319. 连通网络的操作次数
5 实现模板
class UnionFind {
private:vector<int> parent;vector<int> rank; // 或者 sizepublic:UnionFind(int n) {parent.resize(n);rank.resize(n, 1);iota(parent.begin(), parent.end(), 0); // parent[i] = i}// 查找根节点,并进行路径压缩int find(int x) {if (parent[x] != x)parent[x] = find(parent[x]); // 路径压缩return parent[x];}// 合并两个集合void unite(int x, int y) {int px = find(x);int py = find(y);if (px == py) return;// 按秩合并if (rank[px] < rank[py]) {parent[px] = py;} else if (rank[px] > rank[py]) {parent[py] = px;} else {parent[py] = px;rank[px]++;}}// 判断两个元素是否在同一个集合bool connected(int x, int y) {return find(x) == find(y);}
};
6 问题示例:合并等价字符集合(字典序最小)
当我们想用并查集维护字符集合时,可以做如下改造:
- parent[26]:表示字符 ‘a’ 到 ‘z’ 的父节点
- 合并时总是把字典序较小的字符作为根,这样找出的代表字符就是该等价类的最小字母
class Solution {
public:string smallestEquivalentString(string s1, string s2, string baseStr) {vector<int> parent(26);iota(parent.begin(), parent.end(), 0); // a-z 的并查集// 路径压缩查找function<int(int)> find = [&](int x) {if (parent[x] != x)parent[x] = find(parent[x]);return parent[x];};// 合并两个字符等价类,保留字典序较小的作为代表auto unite = [&](int x, int y) {int px = find(x);int py = find(y);if (px == py) return;if (px < py)parent[py] = px;elseparent[px] = py;};for (int i = 0; i < s1.length(); ++i) {unite(s1[i] - 'a', s2[i] - 'a');}string res;for (char c : baseStr) {res += (char)(find(c - 'a') + 'a');}return res;}
};
7 总结
问题特征 | 是否适合使用并查集 |
---|---|
不相交集合合并 | 非常适合(核心用途) |
判断两个元素是否属于同一集合 | 非常高效 |
图的连通性/聚类问题 | 适合 |
频繁增删元素 | 不适合,建议用更复杂的数据结构 |
要维护复杂属性(如路径) | 不适合 |
操作 | 说明 | 时间复杂度 |
---|---|---|
find(x) | 查找 x 所在集合的代表元 | O(α(n)) |
unite(x,y) | 合并两个集合 | O(α(n)) |
connected(x,y) | 判断是否在同一集合 | O(α(n)) |
由于路径压缩和按秩合并优化,几乎所有操作都是近乎常数时间。