并查集:连通性判定与路径压缩

FreeGuideOnline 最新 2026-06-18

并查集:连通性判定与路径压缩

并查集是一种用于管理元素分组关系的高效数据结构,核心功能是动态维护集合的合并与查询。在网络连通、图像分割、最小生成树等场景中,它都能用极低的时间代价回答“这两个节点是否连通”。本教程聚焦其核心机制——路径压缩,帮助你从原理到代码彻底掌握。


什么是并查集

并查集维护一个由若干不相交集合组成的森林。每个集合用一棵树表示,树的根节点代表该集合的“代表元素”。并查集仅提供两个基础操作:

  • 查找 (Find):确定元素属于哪个集合,即找到其所在树的根。
  • 合并 (Union):将两个元素所在的集合合并为一棵树。

正是这两个操作支撑了连通性判定:查询时,只需判断两个元素的根是否相同


基础数据结构

并查集通常使用数组实现。假设有 n 个元素编号 0n-1,数组 parent 记录每个元素的父节点。

初始化:最初每个元素自成一个集合,也就是自己指向自己。

parent = [i for i in range(n)]

此时,森林由 n 棵单节点树构成。


查找操作 (Find)

查找的目标是沿父指针一直向上走,直到遇到根节点(父节点指向自身)。

朴素实现

def find(x):
    while parent[x] != x:
        x = parent[x]
    return x

最坏情况下,树可能退化成链表,单次查找时间复杂度 O(n)。需要优化。


路径压缩:查找即扁平化

路径压缩是并查集的核心优化,思想极其直观:在查找过程中,将沿途遇到的所有节点直接指向根节点。这样做不仅找到了根,还大幅缩短了树的高度,让后续查询几乎变成常数时间。

实现只需在朴素查找的基础上稍作修改:

def find(x):
    if parent[x] != x:
        parent[x] = find(parent[x])  # 递归压缩路径
    return parent[x]

递归版本简洁,但需注意递归深度。若担心栈溢出,可改用迭代+路径重定向:

def find(x):
    root = x
    while parent[root] != root:
        root = parent[root]
    # 第二次遍历,将所有节点直接挂到根
    while x != root:
        nxt = parent[x]
        parent[x] = root
        x = nxt
    return root

无论哪种实现,效果相同:路径上的节点从此直连根,树高几乎恒定为 1


合并操作 (Union)

合并需要先找到两个元素各自的根,然后将其中一个根指向另一个。

def union(x, y):
    root_x = find(x)
    root_y = find(y)
    if root_x != root_y:
        parent[root_x] = root_y  # 简单合并

简单的合并可能导致树高增加。配合路径压缩,即使不加额外优化,平均性能已经极好。


进阶优化:按秩合并

为了让树更平衡,引入“秩”的概念。秩可粗略理解为树高的上界。合并时始终将秩较小的树连接到秩较大的树上,若秩相等则任选并增加秩。

rank = [0] * n

def union(x, y):
    rx = find(x)
    ry = find(y)
    if rx == ry:
        return
    if rank[rx] < rank[ry]:
        parent[rx] = ry
    elif rank[rx] > rank[ry]:
        parent[ry] = rx
    else:
        parent[ry] = rx
        rank[rx] += 1

同时应用路径压缩和按秩合并的并查集,单次操作的摊还时间复杂度接近 O(α(n)),其中 α 为反阿克曼函数,实际应用中可以视为常数时间。


连通性判定流程

连通性判定仅需两步:

  1. 预先将已知连通的点对进行 union
  2. 对任意查询 (a, b),返回 find(a) == find(b)

示例:给定 5 个节点的边 (0,1), (1,2), (3,4),问 0 和 2 是否连通?0 和 3 呢?

n = 5
parent = list(range(n))
edges = [(0,1), (1,2), (3,4)]
for u, v in edges:
    union(u, v)

print(find(0) == find(2))  # True
print(find(0) == find(3))  # False

即使有百万节点,判断连通也只需近乎 O(1) 时间。


复杂度直观理解

优化组合 单次操作平均时间
无优化 O(n)
仅路径压缩 O(log n) 摊还
仅按秩合并 O(log n)
路径压缩 + 按秩合并 O(α(n)) ≈ O(1)

在生产环境中,仅实现路径压缩往往足够快,且代码更短。严谨场景建议两者并用。


典型应用场景

  • 图中连通分量:遍历所有边进行合并,最终不同的根个数即连通分量数。
  • 动态连通性检测:边不断加入,随时查询两点连通性。
  • 最小生成树 (Kruskal 算法):判断加入某边是否会形成环,若不会则合并并加入。
  • 网络冗余 / 社交网络好友关系:圈子合并与归属查询。
  • 等式与不等式矛盾判断:变量相等关系用并查集,不等关系检查是否在同一集合。

完整代码示例 (Python)

class DSU:
    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return
        if self.rank[rx] < self.rank[ry]:
            self.parent[rx] = ry
        elif self.rank[rx] > self.rank[ry]:
            self.parent[ry] = rx
        else:
            self.parent[ry] = rx
            self.rank[rx] += 1

    def connected(self, x, y):
        return self.find(x) == self.find(y)

总结

并查集用极其精简的数组结构解决了集合归属与连通性判定的难题。它的核心在于两点:

  1. 路径压缩让查找过程顺便“修理”树结构,使树趋向扁平。
  2. 按秩合并引导树平衡生长,避免退化。

两者结合后,每个操作几乎都在常数时间内完成。掌握并查集,你就拥有了处理大规模连通性问题的利器。