【无标题】

并查集(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 等)来加深理解。

相关推荐
lly2024066 小时前
Bootstrap 警告框
开发语言
2601_949146536 小时前
C语言语音通知接口接入教程:如何使用C语言直接调用语音预警API
c语言·开发语言
你撅嘴真丑6 小时前
第九章-数字三角形
算法
曹牧6 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
在路上看风景6 小时前
19. 成员初始化列表和初始化对象
c++
KYGALYX6 小时前
服务异步通信
开发语言·后端·微服务·ruby
uesowys6 小时前
Apache Spark算法开发指导-One-vs-Rest classifier
人工智能·算法·spark
zmzb01036 小时前
C++课后习题训练记录Day98
开发语言·c++
ValhallaCoder7 小时前
hot100-二叉树I
数据结构·python·算法·二叉树
董董灿是个攻城狮7 小时前
AI 视觉连载1:像素
算法