C++并查集的实现
使用基础的 C++代码实现一个并查集 (Disjoint Set Union, DSU),有时也称为 Union-Find 数据结构。
并查集主要支持两种操作:
find(i): 查找元素 i 所在的集合的代表元(通常是树的根节点)。
unite(i, j) (或 union_sets(i, j)): 合并元素 i 和元素 j 所在的两个集合。
为了提高效率,我们通常会使用两种优化:
路径压缩 (Path Compression):在 find 操作时,将路径上的所有节点直接指向根节点。
按秩合并 (Union by Rank) 或 按大小合并 (Union by Size):在 unite 操作时,总是将较小的树(按秩或大小)合并到较大的树上,以保持树的平衡。
这里我们先实现一个包含路径压缩的基础版本。按秩/大小合并可以作为后续优化添加。
#include <iostream>
#include <vector>
#include <numeric> // For std::iota (C++11)
class DisjointSetUnion {
private:
std::vector<int> parent; // 存储每个元素的父节点
std::vector<int> rank; // (可选优化) 存储每个集合的秩(树的高度近似值)
// 或者用 size 来代替 rank,表示集合的大小
public:
// 构造函数:初始化n个元素,每个元素自成一个集合
DisjointSetUnion(int n) {
parent.resize(n);
// std::iota(parent.begin(), parent.end(), 0); // C++11: 每个元素的父节点初始为自己
// 如果不用 std::iota,可以手动循环初始化
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
// 如果使用按秩合并或按大小合并,也需要初始化 rank 或 size 数组
rank.assign(n, 0); // 初始秩都为0
// size.assign(n, 1); // 如果按大小合并,初始大小都为1
}
// 查找元素i的代表元(根节点),并进行路径压缩
int find(int i) {
// 如果 parent[i] == i,说明i是根节点
if (parent[i] == i) {
return i;
}
// 路径压缩:将i的父节点直接设置为根节点
// 递归调用find找到根,并将沿途节点的parent直接指向根
return parent[i] = find(parent[i]);
}
// 合并元素i和元素j所在的集合
// 返回true如果成功合并(原本不在同一集合),false如果已在同一集合
bool unite(int i, int j) {
int root_i = find(i); // 找到i的根
int root_j = find(j); // 找到j的根
if (root_i != root_j) { // 如果它们不在同一个集合中
// 将一个根指向另一个根,完成合并
// 这里可以加入按秩合并或按大小合并的优化
if (rank[root_i] < rank[root_j]) { // 按秩合并:将秩小的树合并到秩大的树
parent[root_i] = root_j;
} else if (rank[root_j] < rank[root_i]) {
parent[root_j] = root_i;
} else { // 秩相等,任意合并,并将结果树的秩加1
parent[root_j] = root_i;
rank[root_i]++;
}
// // 如果不使用按秩/大小合并,可以直接:
// parent[root_i] = root_j;
return true;
}
return false; // 它们已经在同一个集合中
}
// (可选) 检查两个元素是否在同一个集合中
bool are_connected(int i, int j) {
return find(i) == find(j);
}
};
int main() {
int num_elements = 10; // 假设有10个元素,编号从0到9
DisjointSetUnion dsu(num_elements);
std::cout << "Initial state (each element is its own set):" << std::endl;
for (int i = 0; i < num_elements; ++i) {
std::cout << "Element " << i << " is in set with root " << dsu.find(i) << std::endl;
}
std::cout << std::endl;
std::cout << "Performing unite operations:" << std::endl;
dsu.unite(0, 1);
std::cout << "Unite(0, 1): Element 0 and 1 are now "
<< (dsu.are_connected(0, 1) ? "connected." : "not connected.") << std::endl;
std::cout << "Root of 0: " << dsu.find(0) << ", Root of 1: " << dsu.find(1) << std::endl;
dsu.unite(2, 3);
std::cout << "Unite(2, 3): Element 2 and 3 are now "
<< (dsu.are_connected(2, 3) ? "connected." : "not connected.") << std::endl;
dsu.unite(0, 2); // 合并0(和1)所在的集合 与 2(和3)所在的集合
std::cout << "Unite(0, 2): Element 0 and 2 are now "
<< (dsu.are_connected(0, 2) ? "connected." : "not connected.") << std::endl;
std::cout << "Element 1 and 3 are now "
<< (dsu.are_connected(1, 3) ? "connected." : "not connected.") << std::endl;
std::cout << "\nCheck connections:" << std::endl;
std::cout << "Are 0 and 3 connected? " << (dsu.are_connected(0, 3) ? "Yes" : "No") << std::endl; // Yes
std::cout << "Are 1 and 2 connected? " << (dsu.are_connected(1, 2) ? "Yes" : "No") << std::endl; // Yes
std::cout << "Are 0 and 4 connected? " << (dsu.are_connected(0, 4) ? "Yes" : "No") << std::endl; // No
dsu.unite(5, 6);
dsu.unite(6, 7);
dsu.unite(5, 8); // 5,6,7,8 都在一个集合
std::cout << "Are 5 and 8 connected? " << (dsu.are_connected(5, 8) ? "Yes" : "No") << std::endl; // Yes
std::cout << "Are 7 and 8 connected? " << (dsu.are_connected(7, 8) ? "Yes" : "No") << std::endl; // Yes
// 尝试合并已经在同一集合的元素
bool merged = dsu.unite(0, 3);
std::cout << "Trying to unite 0 and 3 again, merged: " << (merged ? "Yes (unexpected)" : "No (as expected)") << std::endl;
std::cout << "\nFinal roots after operations:" << std::endl;
for (int i = 0; i < num_elements; ++i) {
std::cout << "Element " << i << " -> final root: " << dsu.find(i) << std::endl;
}
// 经过路径压缩后,多次find同一个元素,其parent会直接指向根
return 0;
}
代码说明:
- parent 向量:
parent[i]存储元素 i 在其所在树中的父节点。
如果parent[i] == i,则元素 i 是其所在集合的代表元(即树的根)。
初始化: 在构造函数中,每个元素最初都是自己的父节点,表示每个元素自成一个集合。std::iota(parent.begin(), parent.end(), 0)是一个方便的方法来初始化parent[i] = i。 - rank 向量 (用于按秩合并):
rank[i] 存储以 i 为根的子树的秩(可以理解为树的高度的一个上界)。
初始化: 初始时,每个集合(单个元素)的秩都为0。
按秩合并逻辑: 当合并两个根不同的集合时,将秩较小的树的根指向秩较大的树的根。如果两个树的秩相同,则任意选一个作为另一个的父节点,并将结果树的秩加1。这有助于保持树的扁平,减少 find 操作的深度。
替代方案:按大小合并 (Union by Size): 你也可以用一个 size 向量来存储每个集合的大小(元素数量),合并时将小集合合并到大集合,并更新大集合的大小。这种方法在某些情况下表现可能更好,实现也类似。
- find(int i) 方法:
基本逻辑: 递归地向上查找,直到找到一个 parent[x] == x 的节点 x,这个 x 就是根。
路径压缩: 在递归返回的过程中,将路径上遇到的所有节点(包括 i)的 parent 直接设置为找到的根节点。
if (parent[i] == i) return i;
return parent[i] = find(parent[i]); // 赋值语句的返回值是被赋的值
这行代码是路径压缩的核心。它不仅返回根,还顺便把 parent[i] 更新为了根。下次再 find(i) 时就会快很多。
unite(int i, int j) 方法:
首先,调用 find(i)和 find(j) 找到它们各自所在集合的代表元 root_i 和 root_j。
如果 root_i == root_j,说明 i 和 j 已经在同一个集合中了,不需要做任何操作,返回 false。
如果 root_i != root_j,则需要合并。这里实现了按秩合并:
比较 rank[root_i] 和 rank[root_j]。
将秩小的树的根的 parent 指向秩大的树的根。
如果秩相等,将 root_j 指向 root_i(或者反过来),并增加 rank[root_i]。
返回 true 表示成功合并。
are_connected(int i, int j) 方法 (可选):
这是一个方便的函数,通过比较 find(i) 和 find(j) 的结果来判断两个元素是否属于同一个连通分量。
这个基础实现包含了路径压缩和按秩合并两种常见的优化,使得并查集操作的平均时间复杂度接近于常数。

浙公网安备 33010602011771号