并查集(Union-Find)是一种用于处理一些不交集(Disjoint Sets)问题的数据结构。它主要支持两种操作:合并集合(Union)和查找元素所属集合(Find)。在解决诸如连通性问题、网络中的群组问题等场景时,并查集表现出色。
并查集的基本思想是使用一个数组来表示所有的元素,数组的每个索引对应一个元素,数组的值则表示该元素所属的集合的代表元素(也称为父节点或根节点)。初始时,每个元素都自成一个集合,所以数组的每个值都初始化为其自身的索引。
查找操作(Find)用于确定两个元素是否属于同一集合,这通常通过递归地查找元素的父节点,直到找到根节点(父节点为自身的节点)为止。在查找过程中,为了加速后续的查找操作,通常还会进行路径压缩,即将查找路径上的所有节点直接指向根节点。
合并操作(Union)用于将两个集合合并为一个集合。这通常通过将其中一个集合的代表元素设置为另一个集合的代表元素的子节点来实现。在实际应用中,为了保持树的平衡性,常常选择秩(rank)较小的树的根节点作为子节点,秩可以简单理解为树的高度的一个上界。
下面是一个简单的C++示例,展示了并查集的基本操作,代码如下。
#include <iostream>
#include <vector>
using namespace std;
class UnionFind {
private:
vector<int> parent; // 父节点数组
vector<int> rank; // 秩数组
public:
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 0);
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];
}
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]++;
}
}
}
bool isConnected(int x, int y) {
return find(x) == find(y);
}
};
int main() {
UnionFind uf(5); // 初始化一个包含5个元素的并查集
uf.unite(0, 1); // 合并0和1所在的集合
uf.unite(2, 3); // 合并2和3所在的集合
cout << uf.isConnected(0, 1) << endl; // 输出1,表示0和1连通
cout << uf.isConnected(0, 2) << endl; // 输出0,表示0和2不连通
uf.unite(1, 3); // 合并1和3所在的集合,由于0和1连通,2和3连通,所以合并后0-1-3-2都连通
cout << uf.isConnected(0, 2) << endl; // 输出1,表示0和2连通
return 0;
}
在上面的示例中,我们首先创建了一个包含5个元素的并查集。然后,我们合并了0和1所在的集合,以及2和3所在的集合。接着,我们检查0和1是否连通(输出1表示连通),以及0和2是否连通(输出0表示不连通)。最后,我们合并了1和3所在的集合,由于之前0和1连通,2和3连通,所以合并后0-1-3-2都连通,再次检查0和2是否连通时输出1表示连通。
并查集的应用实例:朋友圈划分。
假设有n个人,给定他们的m个朋友关系对,如果两个人是朋友,那么他们属于同一个朋友圈。请编写一个程序,输出最终每个人所属的朋友圈编号。为了解决这个问题,我们可以使用并查集数据结构。将每个人视为一个节点,朋友关系对视为边,通过合并操作将属于同一个朋友圈的人合并到同一个集合中。最后,通过查找操作确定每个人所属的朋友圈编号。代码如下。
#include <iostream>
#include <vector>
using namespace std;
class UnionFind {
private:
vector<int> parent; // 父节点数组,初始时每个节点的父节点是自己
vector<int> rank; // 秩数组,记录每个节点对应的树的秩
public:
UnionFind(int n) {
parent.resize(n);
rank.resize(n, 0);
for (int i = 0; i < n; ++i) {
parent[i] = i; // 初始化父节点为自己
}
}
int find(int x) {
if (x != parent[x]) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
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]++;
}
}
}
};
int main() {
int n, m;
cin >> n >> m; // 输入人数和朋友关系对数
UnionFind uf(n); // 初始化并查集
for (int i = 0; i < m; ++i) {
int x, y;
std::cin >> x >> y; // 输入朋友关系对
uf.unite(x - 1, y - 1); // 注意从0开始编号,需要减1转换为从1开始的编号
}
vector<int> circles(n); // 存储每个人所属的朋友圈编号
for (int i = 0; i < n; ++i) {
circles[i] = uf.find(i); // 通过查找操作确定每个人所属的朋友圈编号
circles[i]++; // 由于我们是从0开始编号的,而题目要求从1开始编号,所以加1
}
// 输出每个人所属的朋友圈编号
for (int i = 0; i < n; ++i) {
cout << "第 " << i + 1 << " 人属于朋友圈 " << circles[i] << endl;
}
return 0;
}
假设输入如下:
4 3
1 2
2 3
4 1
表示有4个人,3对朋友关系。运行上述代码后,输出结果如下图所示.
这表明所有人都属于同一个朋友圈,编号为1。
通过并查集的应用,我们可以高效地解决朋友圈划分这类连通性问题。并查集通过合并和查找操作,能够快速地将元素分组,并确定元素之间的关联关系。在实际应用中,并查集还可以用于解决网络中的群组划分、图像分割等问题。