【Algorithm】Union-Find简单介绍

文章目录

  • Union-Find
    • 1 基本概念
      • 1.1 `Find(x)` - 查询操作
      • 1.2 `Union(x, y)` - 合并操作
    • 2 并查集的结构和优化
      • 2.1 数据结构设计
      • 2.2 两大优化策略(关键)
        • 2.2.1 路径压缩(Path Compression)
        • 2.2.2 按秩合并(Union by Rank or Size)
    • 3 使用并查集的注意事项
    • 4 典型应用场景
      • 4.1 判断连通性
      • 4.2 等价类/合并集合
      • 4.3 检测环路(图中是否有环)
      • 4.4 岛屿问题/连通区域
      • 4.5 网络连接问题
    • 5 实现模板
    • 6 问题示例:合并等价字符集合(字典序最小)
    • 7 总结

Union-Find

1 基本概念

并查集是一种用于处理集合合并与查询的数据结构,主要用于解决:

  • 判断两个元素是否属于同一个集合
  • 合并两个集合
  • 图的连通性问题(如 Kruskal 最小生成树、岛屿数量、等价类问题)

核心思想:每个元素初始是一个独立的集合(自成一个树的根)。使用两个操作:

  1. find(x):查找元素 x 所在集合的代表元(根)
  2. union(x, y) / unite(x, y):将元素 x 和 y 所在的两个集合合并为一个

1.1 Find(x) - 查询操作

  • 找出元素 x 所在集合的代表元(也叫根节点、父节点)
  • 判断两个元素是否属于同一个集合:只需比较它们的代表元是否相同

1.2 Union(x, y) - 合并操作

  • 将元素 xy 所在的两个集合合并
  • 目的是把两个集合的元素归于同一个集合(也就是连通)

并查集的本质:将多个不相交的集合合并,并在查询时保持高效


2 并查集的结构和优化

2.1 数据结构设计

  • parent[i]:表示第 i 个元素的父节点(初始时每个元素是自己的父亲)

  • 常见扩展字段:

    • rank[i]:节点的秩(可以理解为树的高度或大小,用于优化合并)
    • size[i]:集合的大小(如果你需要追踪每个集合的元素个数)

2.2 两大优化策略(关键)

2.2.1 路径压缩(Path Compression)
  • 优化 find(x) 操作
  • 将 x 到根节点路径上的所有节点直接指向根,降低后续查找的复杂度
  • 时间复杂度近似 O(α(n)),α 是反阿克曼函数,几乎是常数
int find(int x) {
    if (parent[x] != x)
        parent[x] = find(parent[x]);
    return parent[x];
}
  • 每次查询时,将 x 的所有祖先直接挂到根节点,形成扁平结构
  • 减少下次查找路径长度
2.2.2 按秩合并(Union by Rank or Size)
  • 合并时,总是将“较矮”的树合并到“较高”的树,保持整体平衡,防止链式退化
void union(int x, int y) {
    int px = find(x), py = find(y);
    if (px == py) return;
    if (rank[px] < rank[py])
        parent[px] = py;
    else if (rank[px] > rank[py])
        parent[py] = px;
    else {
        parent[py] = px;
        rank[px]++;
    }
}

3 使用并查集的注意事项

注意事项 说明
初始化 每个元素一开始是自己的父节点(parent[i] = i
找代表元要用 find() 不要直接比较 parent[x] == parent[y],必须比较 find(x) == find(y)
使用路径压缩 提高查找效率,避免变成链表结构
合并要检查代表元 避免重复合并或死循环
不适合有环结构查询 并查集不能表示通用图(除非用于检测是否成环)
不支持高频动态插入删除 并查集适合处理固定集合或批量问题,不适合频繁插入删除

4 典型应用场景

并查集广泛应用于以下场景:

4.1 判断连通性

  • 无向图中判断两个点是否连通
  • 例题:LeetCode 547. 省份数量(朋友圈)

4.2 等价类/合并集合

  • 把多个元素按关系合并为“集合组”
  • 例题:LeetCode 1061. 按字典序排列的最小等价字符串

4.3 检测环路(图中是否有环)

  • 并查集用于无向图的成环检测
  • 例题:Kruskal 最小生成树(MST)

4.4 岛屿问题/连通区域

  • 将二维网格中相邻的“陆地”用并查集合并,统计岛屿数
  • 例题:LeetCode 200. 岛屿数量

4.5 网络连接问题

  • 网络中节点是否连通、连接多少次才能连通
  • 例题:LeetCode 1319. 连通网络的操作次数

5 实现模板

class UnionFind {
private:
    vector<int> parent;
    vector<int> rank;  // 或者 size

public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 1);
        iota(parent.begin(), parent.end(), 0); // 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 px = find(x);
        int py = find(y);
        if (px == py) return;

        // 按秩合并
        if (rank[px] < rank[py]) {
            parent[px] = py;
        } else if (rank[px] > rank[py]) {
            parent[py] = px;
        } else {
            parent[py] = px;
            rank[px]++;
        }
    }

    // 判断两个元素是否在同一个集合
    bool connected(int x, int y) {
        return find(x) == find(y);
    }
};

6 问题示例:合并等价字符集合(字典序最小)

当我们想用并查集维护字符集合时,可以做如下改造:

  • parent[26]:表示字符 ‘a’ 到 ‘z’ 的父节点
  • 合并时总是把字典序较小的字符作为根,这样找出的代表字符就是该等价类的最小字母
class Solution {
public:
    string smallestEquivalentString(string s1, string s2, string baseStr) {
        vector<int> parent(26);
        iota(parent.begin(), parent.end(), 0); // a-z 的并查集

        // 路径压缩查找
        function<int(int)> find = [&](int x) {
            if (parent[x] != x)
                parent[x] = find(parent[x]);
            return parent[x];
        };

        // 合并两个字符等价类,保留字典序较小的作为代表
        auto unite = [&](int x, int y) {
            int px = find(x);
            int py = find(y);
            if (px == py) return;
            if (px < py)
                parent[py] = px;
            else
                parent[px] = py;
        };

        for (int i = 0; i < s1.length(); ++i) {
            unite(s1[i] - 'a', s2[i] - 'a');
        }

        string res;
        for (char c : baseStr) {
            res += (char)(find(c - 'a') + 'a');
        }

        return res;
    }
};

7 总结

问题特征 是否适合使用并查集
不相交集合合并 非常适合(核心用途)
判断两个元素是否属于同一集合 非常高效
图的连通性/聚类问题 适合
频繁增删元素 不适合,建议用更复杂的数据结构
要维护复杂属性(如路径) 不适合
操作 说明 时间复杂度
find(x) 查找 x 所在集合的代表元 O(α(n))
unite(x,y) 合并两个集合 O(α(n))
connected(x,y) 判断是否在同一集合 O(α(n))

由于路径压缩和按秩合并优化,几乎所有操作都是近乎常数时间。

你可能感兴趣的:(C/C++,算法,c++)