【无标题】

并查集(Disjoint Set Union)详解:原理、优化与应用

并查集(也称为不相交集合数据结构 ,英文 Disjoint Set Union 或 Union-Find)是一种非常高效的数据结构,主要用于处理动态连通性问题。它支持两种核心操作:

  • 查询(Find):判断两个元素是否属于同一个集合(即是否连通)。
  • 合并(Union):将两个集合合并成一个。

并查集在算法竞赛(如 OI、ACM)和实际工程中应用广泛,例如最小生成树(Kruskal 算法)、图的连通分量统计、网格连通问题等。其最大特点是操作几乎达到 O(1) 时间复杂度(摊销后)。

本文将从基本原理入手,逐步介绍实现、优化技巧,并附上完整代码示例。

1. 并查集的基本原理

并查集用一个森林 (多棵树)来表示多个不相交的集合。每棵树代表一个集合,树的根节点作为该集合的代表元。

  • 用一个数组 parent[] 表示每个节点的父节点。
  • 初始时,每个元素都是独立的集合:parent[i] = i
  • 查找根节点(Find 操作):从某个节点向上追溯,直到找到父节点为自己本身的节点(根)。
  • 合并(Union 操作):找到两个元素的根节点,如果不同,则将一棵树的根指向另一棵树的根。

简单实现(无优化)

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int parent[N];

void init(int n) {
    for (int i = 1; i <= n; i++) {
        parent[i] = i;
    }
}

int find(int x) {
    if (parent[x] == x) return x;
    return find(parent[x]);  // 递归查找根
}

void union_sets(int x, int y) {
    int rx = find(x);
    int ry = find(y);
    if (rx != ry) {
        parent[rx] = ry;  // 将 rx 的根指向 ry 的根
    }
}

这种实现最坏情况下树会退化成一条链(高度为 n),每次 Find 操作可能需要 O(n) 时间。

2. 优化一:路径压缩(Path Compression)

路径压缩的核心思想是:在查找根节点的过程中,将路径上的所有节点直接指向根节点,从而压扁树的高度。

修改 Find 函数:

cpp 复制代码
int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);  // 递归压缩路径
    }
    return parent[x];
}

效果示意:

  • 未压缩:1 → 2 → 3 → 4(根)
  • 压缩后:1、2、3 都直接指向 4

路径压缩后,树的高度会大幅降低,后续查询极快。

3. 优化二:按秩合并(Union by Rank)

单纯路径压缩虽然优秀,但合并时如果总是随意连接,可能导致树偏斜。按秩合并通过维护一个 rank[] 数组记录每棵树的"秩"(近似高度)来优化。

  • 初始时,所有秩为 1(或 0)。
  • 合并时,总是将秩小的树挂到秩大的树上
  • 如果秩相等,任选一个挂接,并将胜出方的秩 +1。
cpp 复制代码
int rank[N];

void init(int n) {
    for (int i = 1; i <= n; i++) {
        parent[i] = i;
        rank[i] = 1;  // 初始秩为 1
    }
}

void union_sets(int x, int y) {
    int rx = find(x);
    int ry = find(y);
    if (rx == ry) return;
    // 将秩小的合并到秩大的分支上
    if (rank[rx] < rank[ry]) {
        parent[rx] = ry;
    } else {
        parent[ry] = rx;
        if (rank[rx] == rank[ry]) {
            rank[rx]++;
        }
    }
}

路径压缩 + 按秩合并 是标准优化组合,摊销时间复杂度为 O(α(n)),其中 α(n) 是反阿克曼函数,增长极慢,实际可视为常数。

4. 完整优化版代码

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int parent[N];
int rank[N];

void init(int n) {
    for (int i = 1; i <= n; i++) {
        parent[i] = i;
        rank[i] = 1;
    }
}

int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);  // 路径压缩
    }
    return parent[x];
}

void union_sets(int x, int y) {
    int rx = find(x);
    int ry = find(y);
    if (rx == ry) return;
    
    if (rank[rx] < rank[ry]) {
        swap(rx, ry);  // 保证 rx 的秩更大
    }
    parent[ry] = rx;
    if (rank[rx] == rank[ry]) {
        rank[rx]++;
    }
}

int main() {
    // 示例使用
    init(10);
    union_sets(1, 2);
    union_sets(3, 4);
    union_sets(2, 4);
    cout << (find(1) == find(4) ? "连通" : "不连通") << endl;  // 输出:连通
    return 0;
}

5. 常见应用场景

  1. 图的连通分量统计:遍历所有边进行 Union,最后统计根节点数量。
  2. Kruskal 最小生成树:按边权排序,Union 时判断是否形成环。
  3. 网格/迷宫连通问题:如 LeetCode 的"朋友圈"、"被围绕的区域"等。
  4. 在线判断连通性:动态添加边,实时查询是否连通。

6. 扩展:按大小合并(Union by Size)

另一种优化是维护每个集合的大小 size[],合并时将小集合挂到大集合上。效果与按秩合并类似,有时更优(尤其在需要统计集合大小时)。

cpp 复制代码
int size[N];

void init(int n) {
    for (int i = 1; i <= n; i++) {
        parent[i] = i;
        size[i] = 1;
    }
}

void union_sets(int x, int y) {
    int rx = find(x), ry = find(y);
    if (rx == ry) return;
    if (size[rx] < size[ry]) swap(rx, ry);
    parent[ry] = rx;
    size[rx] += size[ry];
}

总结

并查集是算法竞赛中的"神器"之一,实现简单却效率极高。记住两大优化:

  • 路径压缩:让 Find 更快。
  • 按秩/大小合并:防止树退化。

掌握了并查集,很多图论和连通性问题都会迎刃而解。建议多刷相关题目(如 HDU 1232 畅通工程、POJ 2236 等)来加深理解。

相关推荐
董董灿是个攻城狮5 分钟前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者33 分钟前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考40 分钟前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx4 小时前
CART决策树基本原理
算法·机器学习
Wect4 小时前
LeetCode 210. 课程表 II 题解:Kahn算法+DFS 双解法精讲
前端·算法·typescript
颜酱5 小时前
单调队列:滑动窗口极值问题的最优解(通用模板版)
javascript·后端·算法
肆忆_8 小时前
# 用 5 个问题学懂 C++ 虚函数(入门级)
c++
不想写代码的星星12 小时前
虚函数表:C++ 多态背后的那个男人
c++
Gorway12 小时前
解析残差网络 (ResNet)
算法
拖拉斯旋风12 小时前
LeetCode 经典算法题解析:优先队列与广度优先搜索的巧妙应用
算法