目录
[1.1 什么是并查集](#1.1 什么是并查集)
[1.2 核心思想](#1.2 核心思想)
[2.1 结构定义](#2.1 结构定义)
[2.2 初始化](#2.2 初始化)
[2.3 查找(Find)](#2.3 查找(Find))
[2.4 合并(Union)](#2.4 合并(Union))
[3.1 问题分析](#3.1 问题分析)
[3.2 路径压缩](#3.2 路径压缩)
[4.1 问题分析](#4.1 问题分析)
[4.2 按秩合并](#4.2 按秩合并)
[6.1 问题描述](#6.1 问题描述)
[6.2 代码实现](#6.2 代码实现)
一、并查集的基本概念
1.1 什么是并查集
并查集(Union-Find)维护一组不相交的集合,支持:
-
查找(Find):确定元素属于哪个集合
-
合并(Union):将两个集合合并为一个
应用场景:
| 场景 | 说明 |
|---|---|
| 朋友圈问题 | 判断两人是否在同一社交圈 |
| Kruskal算法 | 判断添加边是否会形成环 |
| 连通分量 | 统计图中连通块数量 |
| 动态连通性 | 网络连接、道路连通 |
1.2 核心思想
用树表示集合,树的根节点代表这个集合。每个元素存储其父节点,根节点的父节点指向自己。
text
初始:每个元素独立
0 1 2 3 4
合并{0,2}后:
0
|
2
合并{1,3}后:
1
|
3
合并{2,3}后(合并两个集合):
0
/ \
2 1
|
3
二、基础实现
2.1 结构定义
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 1000
typedef struct {
int parent[MAX_SIZE]; // 父节点数组
int rank[MAX_SIZE]; // 树的高度(用于优化)
} UnionFind;
2.2 初始化
每个元素自成一个集合,父节点指向自己,秩为0。
c
void ufInit(UnionFind *uf, int n) {
for (int i = 0; i < n; i++) {
uf->parent[i] = i;
uf->rank[i] = 0;
}
}
2.3 查找(Find)
找到根节点(父节点指向自己的节点)。
c
int ufFind(UnionFind *uf, int x) {
while (uf->parent[x] != x) {
x = uf->parent[x];
}
return x;
}
2.4 合并(Union)
将两个集合的根节点连接,把一棵树的根指向另一棵树的根。
c
void ufUnion(UnionFind *uf, int x, int y) {
int rootX = ufFind(uf, x);
int rootY = ufFind(uf, y);
if (rootX != rootY) {
uf->parent[rootY] = rootX;
}
}
三、优化:路径压缩
3.1 问题分析
基础版本的Find在最坏情况下会退化成链表,查找复杂度O(n)。
text
合并顺序:0-1, 1-2, 2-3, 3-4
树结构:0 → 1 → 2 → 3 → 4
查找4需要遍历5次
3.2 路径压缩
在查找过程中,将路径上的所有节点直接指向根节点。
c
// 递归写法(更简洁)
int ufFind(UnionFind *uf, int x) {
if (uf->parent[x] != x) {
uf->parent[x] = ufFind(uf, uf->parent[x]);
}
return uf->parent[x];
}
// 非递归写法
int ufFind(UnionFind *uf, int x) {
int root = x;
while (uf->parent[root] != root) {
root = uf->parent[root];
}
// 路径压缩
while (x != root) {
int next = uf->parent[x];
uf->parent[x] = root;
x = next;
}
return root;
}
四、优化:按秩合并
4.1 问题分析
路径压缩已经很快,但Union时如果随意连接,可能导致树高度增加。
4.2 按秩合并
将高度小的树接到高度大的树下,避免树变高。
c
void ufUnion(UnionFind *uf, int x, int y) {
int rootX = ufFind(uf, x);
int rootY = ufFind(uf, y);
if (rootX == rootY) return;
// 按秩合并:高度小的接在高度大的下面
if (uf->rank[rootX] < uf->rank[rootY]) {
uf->parent[rootX] = rootY;
} else if (uf->rank[rootX] > uf->rank[rootY]) {
uf->parent[rootY] = rootX;
} else {
uf->parent[rootY] = rootX;
uf->rank[rootX]++;
}
}
五、完整代码实现
c
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 1000
typedef struct {
int parent[MAX_SIZE];
int rank[MAX_SIZE];
} UnionFind;
// 初始化
void ufInit(UnionFind *uf, int n) {
for (int i = 0; i < n; i++) {
uf->parent[i] = i;
uf->rank[i] = 0;
}
}
// 查找(路径压缩)
int ufFind(UnionFind *uf, int x) {
if (uf->parent[x] != x) {
uf->parent[x] = ufFind(uf, uf->parent[x]);
}
return uf->parent[x];
}
// 合并(按秩合并)
void ufUnion(UnionFind *uf, int x, int y) {
int rootX = ufFind(uf, x);
int rootY = ufFind(uf, y);
if (rootX == rootY) return;
if (uf->rank[rootX] < uf->rank[rootY]) {
uf->parent[rootX] = rootY;
} else if (uf->rank[rootX] > uf->rank[rootY]) {
uf->parent[rootY] = rootX;
} else {
uf->parent[rootY] = rootX;
uf->rank[rootX]++;
}
}
// 判断两个元素是否在同一集合
int ufConnected(UnionFind *uf, int x, int y) {
return ufFind(uf, x) == ufFind(uf, y);
}
// 统计集合数量
int ufCount(UnionFind *uf, int n) {
int count = 0;
for (int i = 0; i < n; i++) {
if (uf->parent[i] == i) count++;
}
return count;
}
// 打印每个元素所属的根
void printSets(UnionFind *uf, int n) {
for (int i = 0; i < n; i++) {
printf("%d -> %d\n", i, ufFind(uf, i));
}
}
int main() {
UnionFind uf;
int n = 10;
ufInit(&uf, n);
printf("=== 初始状态 ===\n");
printf("集合数量: %d\n", ufCount(&uf, n));
// 合并操作
ufUnion(&uf, 0, 1);
ufUnion(&uf, 2, 3);
ufUnion(&uf, 0, 2);
ufUnion(&uf, 4, 5);
ufUnion(&uf, 6, 7);
ufUnion(&uf, 8, 9);
ufUnion(&uf, 5, 6);
printf("\n=== 合并后 ===\n");
printf("集合数量: %d\n", ufCount(&uf, n));
printf("\n元素关系:\n");
printSets(&uf, n);
printf("\n=== 连通性查询 ===\n");
printf("0和3是否连通: %s\n", ufConnected(&uf, 0, 3) ? "是" : "否");
printf("4和7是否连通: %s\n", ufConnected(&uf, 4, 7) ? "是" : "否");
printf("4和8是否连通: %s\n", ufConnected(&uf, 4, 8) ? "是" : "否");
return 0;
}
运行结果:
text
=== 初始状态 ===
集合数量: 10
=== 合并后 ===
集合数量: 3
元素关系:
0 -> 0
1 -> 0
2 -> 0
3 -> 0
4 -> 4
5 -> 4
6 -> 4
7 -> 4
8 -> 8
9 -> 8
=== 连通性查询 ===
0和3是否连通: 是
4和7是否连通: 是
4和8是否连通: 否
六、经典应用:朋友圈问题
6.1 问题描述
有n个人,给定m对好友关系(互为好友),求有多少个朋友圈(连通分量)。
6.2 代码实现
c
// 统计朋友圈数量
int friendCircles(int n, int edges[][2], int edgeCount) {
UnionFind uf;
ufInit(&uf, n);
for (int i = 0; i < edgeCount; i++) {
ufUnion(&uf, edges[i][0], edges[i][1]);
}
return ufCount(&uf, n);
}
int main() {
// 5个人,好友关系:0-1, 1-2, 3-4
int edges[][2] = {{0, 1}, {1, 2}, {3, 4}};
int n = 5;
int edgeCount = sizeof(edges) / sizeof(edges[0]);
int circles = friendCircles(n, edges, edgeCount);
printf("朋友圈数量: %d\n", circles);
return 0;
}
运行结果:
text
朋友圈数量: 2
七、Kruskal算法中的并查集
c
// Kruskal算法中判断边是否会形成环
typedef struct {
int u, v, weight;
} Edge;
int kruskalMST(Edge edges[], int n, int edgeCount) {
// 按权值排序
qsort(edges, edgeCount, sizeof(Edge), cmpEdge);
UnionFind uf;
ufInit(&uf, n);
int totalWeight = 0;
int selectedEdges = 0;
for (int i = 0; i < edgeCount && selectedEdges < n - 1; i++) {
int u = edges[i].u;
int v = edges[i].v;
// 如果不在同一集合,加入MST
if (ufFind(&uf, u) != ufFind(&uf, v)) {
ufUnion(&uf, u, v);
totalWeight += edges[i].weight;
selectedEdges++;
}
}
return totalWeight;
}
八、复杂度分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 初始化 | O(n) | 遍历所有元素 |
| 查找(未优化) | O(n) | 可能退化成链表 |
| 查找(路径压缩) | O(α(n)) | 接近O(1) |
| 合并(未优化) | O(n) | 需要先查找 |
| 合并(按秩+路径压缩) | O(α(n)) | 接近O(1) |
α(n) 是阿克曼函数的反函数,增长极慢,n=10^80时 α(n)≤5
九、路径压缩的可视化
c
// 演示路径压缩效果
void demoPathCompression() {
UnionFind uf;
ufInit(&uf, 5);
// 构造一条链 0->1->2->3->4
uf.parent[1] = 0;
uf.parent[2] = 1;
uf.parent[3] = 2;
uf.parent[4] = 3;
printf("压缩前:\n");
for (int i = 0; i < 5; i++) {
printf("parent[%d]=%d\n", i, uf.parent[i]);
}
// 查找4,触发路径压缩
ufFind(&uf, 4);
printf("\n压缩后:\n");
for (int i = 0; i < 5; i++) {
printf("parent[%d]=%d\n", i, uf.parent[i]);
}
}
运行结果:
text
压缩前:
parent[0]=0
parent[1]=0
parent[2]=1
parent[3]=2
parent[4]=3
压缩后:
parent[0]=0
parent[1]=0
parent[2]=0
parent[3]=0
parent[4]=0
十、小结
这一篇我们学习了并查集:
| 操作 | 实现 | 优化 |
|---|---|---|
| 初始化 | parent[i]=i | - |
| 查找 | 找根节点 | 路径压缩 |
| 合并 | 根相连 | 按秩合并 |
核心代码:
c
int find(int x) {
return parent[x] == x ? x : (parent[x] = find(parent[x]));
}
void union(int x, int y) {
int rx = find(x), ry = find(y);
if (rx != ry) parent[ry] = rx;
}
适用场景:
-
动态连通性判断
-
最小生成树(Kruskal)
-
朋友圈、岛屿数量
下一篇我们讲Trie树(前缀树)。
十一、思考题
-
路径压缩和按秩合并分别优化了什么?可以只用其中一个吗?
-
并查集的查找和合并操作的时间复杂度为什么是O(α(n))?
-
如何用并查集判断一个图是否有环?
-
在Kruskal算法中,为什么用并查集来判断环比DFS更高效?
欢迎在评论区讨论你的答案。