一、为什么需要并查集?
社交网络问题:
- 小明和小红是朋友,小红和小丽是朋友,那么小明和小丽是否在同一个朋友圈里?
- 如何快速判断两个人是否能通过朋友关系间接认识?
团队划分:
- 班级里要分组做项目,已知一些同学必须在同一组,如何确定最终能分成几个组?
这些问题的共同特点是:
- 需要维护元素之间的关系(连通/属于同一组)
- 需要快速查询两个元素是否有关系
- 需要动态合并两个组/集合
- 需要统计总共有多少个独立的组
并查集这种数据结构在图论(例如 Kruskal 求最小生成树)、连通分量统计、网络连通性检测等场景非常常用。
二、什么是并查集?
并查集(Union-Find Set),也称为不相交集合数据结构(Disjoint Set Data Structure),是一种树型的数据结构,用于处理一些不交集的合并及查询问题。它的两个核心操作:
- Find(查找):确定某个元素属于哪个子集
- Union(合并):将两个子集合并成一个集合
核心思想:用树形结构来表示集合,每个集合用一棵树来表示,树的根节点就是该集合的代表元素。所有属于同一个集合的元素最终都能追溯到同一个根节点。
三、基本实现
3.1 朴素实现
cpp
class UnionFindSet
{
public:
UnionFindSet(size_t n)
:_ufs(n, -1)
{ }
int FindRoot(int x) //查找根
{
int parent = x;
while (_ufs[parent] >= 0)
{
parent = _ufs[parent];
}
return parent;
}
void Union(int x, int y) //合并节点
{
int root_x = FindRoot(x);
int root_y = FindRoot(y);
if (root_x == root_y)
return;
_ufs[root_x] += _ufs[root_y];
_ufs[root_y] = root_x;
}
bool InSet(int x, int y) //俩个节点是否在同一个集合
{
return FindRoot(x) == FindRoot(y);
}
size_t SetSize()
{
int count = 0;
for (auto x : _ufs)
{
if (x < 0) count++;
}
return count;
}
private:
vector<int> _ufs;
};
3.2 朴素实现的问题
朴素实现存在一个严重问题:树可能退化成链表
假设我们依次合并 (1,2), (2,3), (3,4), (4,5):1 → 2 → 3 → 4 → 5
这种情况下,查找元素1的根节点需要O(n)的时间复杂度,效率很低。
四、优化策略
4.1 路径压缩(Path Compression)
核心思想:在查找根节点的过程中,将路径上的所有节点都直接连接到根节点。
cpp
int FindRoot(int x)
{
if (parent[x] != x)
{
parent[x] = FindRoot(parent[x]); // 路径压缩
}
return parent[x];
}
4.2 按秩合并(Union by Rank)
按秩合并的思想:总是将较小的树连接到较大的树上,保持树的平衡。
cpp
class UnionFind {
private:
vector<int> parent, rank; // rank[i]表示以i为根的树的高度
public:
UnionFind(int n) : parent(n), rank(n, 0)
{
for (int i = 0; i < n; i++)
{
parent[i] = i;
}
}
void unite(int x, int y)
{
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY)
{
if (rank[rootX] < rank[rootY])
{
parent[rootX] = rootY;
}
else if (rank[rootX] > rank[rootY])
{
parent[rootY] = rootX;
}
else
{
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
};
五、完整的优化实现
cpp
class OptimizedUnionFind {
private:
vector<int> parent, rank;
int components; // 连通分量个数
public:
OptimizedUnionFind(int n) : parent(n), rank(n, 0), components(n) {
for (int i = 0; i < n; i++) {
parent[i] = i;
}
}
int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
bool unite(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return false; // 已经连通
// 按秩合并
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
components--; // 连通分量减少
return true;
}
bool connected(int x, int y) {
return find(x) == find(y);
}
int getComponents() {
return components;
}
};
十、总结
并查集是一个看似简单但功能强大的数据结构:
优点:
高效:经过优化后,操作时间复杂度接近常数
简洁:代码实现相对简单
通用:适用于多种连通性问题
通过合理的优化,并查集能够在近似常数时间内完成查找和合并操作,是解决连通性问题的首选数据结构。