并查集:连通性问题的高效解法

数据结构学到这里,你会慢慢发现:并不是所有问题都在问“怎么查找某个值”“怎么维护顺序”“怎么取最小最大”。还有一类问题问得非常直接,也非常高频:两个元素是不是属于同一组?一堆对象之间是否连通?加入一条边之后,哪些点被合并到了同一个集合里?

这类问题如果每次都现算图遍历,当然也能做,但代价往往偏高。并查集之所以经典,就是因为它专门为这类“动态维护若干不相交集合,并快速判断归属关系”的问题设计。它不是传统意义上最直观的树或表结构,但它在图算法、网络连通性、社交关系分组、最小生成树等场景里非常高频。

所以学并查集时,最重要的不是把几个函数名记住,而是先看清它到底解决什么问题:

一边不断合并集合,一边高效判断两个元素是否已经属于同一集合。

一、什么是并查集

并查集,英文通常叫 Disjoint Set Union,简称 DSUUnion-Find。它维护的是若干个互不相交的集合,并支持两类核心操作:

  • find(x):查找元素 x 所属集合的代表;
  • union(x, y):把 xy 所属的两个集合合并。

如果两个元素 find 出来的代表相同,就说明它们已经属于同一个集合。

所以并查集最本质的事情是:

  • 管理分组;
  • 支持合并;
  • 快速判断是否同组。

二、为什么普通结构不够高效

假设你有很多元素和一些连接关系,想不断回答:

  • ab 连通吗?
  • 加入这条连接后,原来的两个集合是否应该合并?

如果每次都:

  • 重新做一次 DFS / BFS
  • 或者暴力扫描整个分组信息;

那么在大量动态合并场景下,成本会很高。

并查集的价值就在于:

  • 让集合合并变得局部可维护;
  • 让连通性判断几乎接近常数时间。

三、并查集通常怎么表示集合

并查集最经典的底层思路,是用一棵树来表示一个集合。

  • 每个元素是一个节点;
  • 每个集合对应一棵树;
  • 树根可以看作这个集合的代表元。

于是:

  • find(x) 就是一路向上找到根;
  • union(x, y) 就是把一棵树的根接到另一棵树根上。

所以并查集虽然名字里有“集”,但它底层其实常用森林结构来维护。

四、什么是代表元

代表元可以理解成:

当前集合的一个标识节点,通常就是树根。

它并不一定携带特别业务语义,关键作用是:

  • 作为集合身份的统一标记;
  • 只要两个元素找到同一个根,就说明它们同组。

所以并查集判断连通的核心逻辑特别简单:

1
find(x) == find(y)

五、并查集最基本的三种操作

1. 初始化

一开始,每个元素各自属于一个独立集合,也就是每个节点自己是自己的根。

2. 查找 find

从当前节点一直沿父节点往上,直到找到根节点。

3. 合并 union

先分别找到两个元素的根;
如果根不同,就把一个根挂到另一个根下面,完成集合合并。

这三个动作构成了并查集最基础的工作流。

六、为什么朴素并查集还不够快

如果你只是简单地把一个根挂到另一个根下面,树形可能会越来越高。

例如如果总是:

  • 把新集合接到某个固定根下;
  • 或者形成一条长链;

find 操作就会变慢,因为每次都要走很多层。

所以并查集真正高效,靠的不是最朴素版本,而是两个关键优化:

  • 路径压缩;
  • 按秩合并或按大小合并。

七、什么是路径压缩

路径压缩的核心思想是:

在执行 find(x) 的过程中,顺手把路径上的节点都直接挂到根上。

例如你原本从 x 找根,需要走很多步;那找完之后,不妨把这一路上的节点父指针都改成根。这样下次再查这些节点时,路径就会短很多。

这是一种典型的“查询时顺手优化结构”的思路。

它的意义非常大,因为在大量连续操作中,路径压缩能让树迅速扁平化。

八、什么是按秩合并或按大小合并

这类优化的目标是:

合并时尽量别让树长得太高。

常见做法有两种:

1. 按大小合并

把小集合挂到大集合根下面。

2. 按秩合并

把高度较低或近似层数较小的树,挂到更高的树下面。

不管是哪一种,本质上都是在尽量控制树高增长。

九、路径压缩和按秩合并一起用会怎样

这正是并查集之所以强大的原因。

  • 按秩合并从源头上减少树变高的机会;
  • 路径压缩在查询过程中进一步把树压平。

两者一起使用后,并查集的单次操作复杂度在均摊意义上可以做到非常接近常数时间。严格分析会涉及反阿克曼函数,但在实际应用里通常可以把它看成“几乎 O(1)”。

所以并查集之所以在图论和连通性问题里高频,不是因为它花哨,而是因为它真的很高效。

十、并查集最典型的应用场景有哪些

1. 动态连通性判断

给定若干点和连接操作,反复问两个点是否连通。

2. 图中连通分量维护

不断合并边连接的点,最后统计有多少个连通块。

3. Kruskal 最小生成树

这是并查集最经典的算法应用之一。每加入一条边前,只要判断两个端点是否已在同一集合,就能知道这条边会不会形成环。

4. 社交关系、群组归并

谁和谁属于同一圈子、同一组织、同一连通网络。

5. 等价类问题

例如多个对象通过若干关系被判定为“同类”。

这些问题共同的味道就是:

  • 不断合并;
  • 不断问是否同组。

十一、并查集和图遍历是什么关系

它们都能处理连通性问题,但适用场景不同。

1. DFS / BFS

更适合:

  • 你已经有完整图结构;
  • 想从一个起点遍历整块区域;
  • 或者想拿到具体路径、访问顺序。
2. 并查集

更适合:

  • 你关心的是“是否同组”;
  • 连接关系在动态加入;
  • 需要高频判断集合归属。

所以并查集并不是“替代图遍历”,而是专门针对连通性维护做优化的一种结构。

十二、并查集有什么局限

1. 不擅长删除关系

经典并查集很适合合并,不适合把已经合并的集合再拆开。

2. 不直接给出路径信息

它只告诉你是否属于同一集合,通常不告诉你具体怎么连通。

3. 更适合静态或单向增加连接的场景

如果关系频繁增删,需要更复杂的数据结构。

这说明并查集虽然高效,但优化得非常专注:

它擅长的是“合并 + 判断同组”,而不是所有图问题。

十三、学习并查集时最容易踩的坑

1. 只会背模板,不理解集合树表示法

如果不理解“每个集合是一棵树”,很多优化就会显得像魔法。

2. 忘记先找根再合并

union 的核心是合并代表元,不是随便把两个普通节点连起来。

3. 不理解路径压缩为什么正确

路径压缩不会改变集合归属,只是在保持代表元不变的前提下缩短查询路径。

4. 把并查集用到需要删除和精确路径的问题上

这超出了它的强项。

总结

并查集的重要性,不只是因为它在算法题里高频,而是因为它把“动态维护连通性”这件事抽象得非常高效。真正值得先建立起来的,是这些核心认识:

  • 并查集维护的是若干互不相交的集合;
  • find 负责找到代表元,union 负责合并集合;
  • 用森林表示集合后,连通性判断可以转化为“是否同根”;
  • 路径压缩和按秩合并,是并查集高效的关键;
  • 它特别适合动态合并和同组判断问题,例如 Kruskal、连通块维护、等价类归并;
  • 真正学会并查集,不是背一段模板,而是知道它到底在优化什么、又不擅长什么。

把这一篇理解透之后,后面再去学图算法时,你会更容易看清:有些问题需要遍历整张图,有些问题只需要维护“是不是连着”。而后者,正是并查集最擅长的领域。

参考资源