并查集 (Union-Find)
并查集:数字世界的官僚
算法背后的故事:派对策划人的难题
想象你正在举办一场有 1000 位客人的大型派对。你想知道两个随机的人,Alice 和 Bob,是否属于同一个社交圈。
- Alice 认识 Carol。
- Carol 认识 Dave。
- Dave 认识 Bob。
如果每次都要顺着这条链条去查,效率会非常低 ()。
1964 年,Bernard Galler 和 Michael Fisher 提出了一种类似于 政党组织 的解决方案:每个人都属于一个小组,每个小组都有一个 “代表” (The Boss)。
如果你问 Alice:“谁是你的老大?”,她说“Carol”。问 Carol,她说“Dave”。问 Dave,他说“是我自己”。 如果你问 Bob:“谁是你的老大?”,他最终也指向“Dave”。 既然他们的老大是同一个人,他们就在同一个圈子里。非常简单。
但他们加入了一个叫做 路径压缩 (Path Compression) 的天才设计:下一次你再问 Alice 时,她会跳过中间人,直接指着 Dave 说:“他是老大。” 这个简单的技巧将层级结构彻底扁平化,使得查询几乎是瞬间完成的。
为什么需要它?
并查集只为一件事而生:动态连通性 (Dynamic Connectivity)。
- 社交网络: “你和 X 有 5 个共同好友”背后的连通图。
- 图像处理: Photoshop 的“魔棒”工具使用它来选中所有颜色相同的相连像素。
- Kruskal 算法: 在构建最小生成树(用最省钱的电缆连接城市)时,需要不断检查“这两个城市是否已经连通了?”
算法是如何“思考”的
这个算法就像一个只有两项工作的官僚:
Find (询问): 它问一个节点:“你的领导是谁?” 如果这个节点指向别人,它就顺藤摸瓜往上找,直到找到那个指向自己的“大老板”。 关键优化: 在它回来的路上,它会悄悄告诉链路上的每个人:“顺便说一句,大老板是 Dave。下次别再层层汇报了,直接找他。” (路径压缩)
Union (合并): 它将两个圈子合并。它找到 A 圈子的老大,对他说:“从现在起,你向 B 圈子的老大汇报。” 优化: 它总是让小圈子向大圈子汇报,以保持树的层级尽可能浅。(按秩合并)
“反阿克曼函数”的奇迹
并查集以其速度闻名。它的时间复杂度是 ,其中 是反阿克曼函数。 这是什么概念? 即使 是宇宙中原子的总数, 也不会超过 4。 在工程实践中,它实际上就是 常数时间 。
实现参考 (Python)
class UnionFind:
def __init__(self, size):
# 初始化:每个人的老大都是自己
self.parent = list(range(size))
# Rank 用于优化合并策略(谁听谁的)
self.rank = [1] * size
def find(self, p):
# 1. 路径压缩:官僚主义的捷径
if self.parent[p] != p:
# 递归找到大老板,并将我的直接上级改为大老板
self.parent[p] = self.find(self.parent[p])
return self.parent[p]
def union(self, p, q):
# 2. 按秩合并:小弟听大哥的
rootP = self.find(p)
rootQ = self.find(q)
if rootP != rootQ:
# 将较矮的树挂在较高的树下面
if self.rank[rootP] > self.rank[rootQ]:
self.parent[rootQ] = rootP
elif self.rank[rootP] < self.rank[rootQ]:
self.parent[rootP] = rootQ
else:
self.parent[rootQ] = rootP
self.rank[rootP] += 1
return True # 合并成功
return False # 已经是自己人了
# 使用示例
# uf = UnionFind(10)
# uf.union(1, 2)
# uf.union(2, 5)
# print(uf.find(1) == uf.find(5)) # True小结
并查集教会我们,管理复杂组织最好的方法就是 扁平化层级。通过赋予每个成员直接知晓“大老板”的能力,它将一团乱麻的关系网变成了一个光速查询系统。
