7.3 併查集Union-Find#
概念說明#
併查集(Union-Find / Disjoint Set) 管理「把 n 個元素劃分成若干互斥集合」,只支援兩個操作:
find(x):x 屬於哪一組?(回傳該集合的「代表」)union(x, y):把 x、y 所在的兩組合併成一組。
它不支援隨意刪除、不記錄組內明細,但換來近乎 O(1) 的單次操作——這是資料結構史上「化約問題、極簡致勝」的最佳範例。經典應用:Kruskal 最小生成樹、判斷圖的連通性、社群好友圈合併、迷宮生成。
關鍵優化#
用「森林」表示:每個集合是一棵樹,parent[i] 指向父節點,根節點就是代表。兩個優化缺一不可:
- 路徑壓縮(path compression):
find時順手把沿途節點全部直接掛到根上,樹越壓越扁。 - 按秩/大小合併(union by rank/size):合併時把矮樹掛到高樹下,避免樹越長越高。
單獨任一優化是 O(log n);兩者並用,n 次操作的總成本為 O(n·α(n)),α 是反阿克曼函數——在現實宇宙中 α(n) ≤ 4,可視為常數。
Python 實作#
class UnionFind:
def __init__(self, n):
self.parent = list(range(n)) # 起初各自成組
self.rank = [0] * n
self.count = n # 組數
def find(self, x):
root = x
while self.parent[root] != root: # 先找到根
root = self.parent[root]
while self.parent[x] != root: # 路徑壓縮:沿途全掛到根
self.parent[x], x = root, self.parent[x]
return root
def union(self, x, y):
rx, ry = self.find(x), self.find(y)
if rx == ry:
return False # 已同組(可用於偵測環)
if self.rank[rx] < self.rank[ry]: # 按秩合併:矮掛高
rx, ry = ry, rx
self.parent[ry] = rx
if self.rank[rx] == self.rank[ry]:
self.rank[rx] += 1
self.count -= 1
return True
def same_set(self, x, y):
return self.find(x) == self.find(y)
# 好友圈問題:0..7 八個人
uf = UnionFind(8)
pairs = [(0, 1), (1, 2), (3, 4), (5, 6), (2, 7), (0, 7)]
for a, b in pairs:
uf.union(a, b)
print("0 和 7 同圈?", uf.same_set(0, 7)) # True
print("3 和 5 同圈?", uf.same_set(3, 5)) # False
print("好友圈數:", uf.count) # 3 → {0,1,2,7} {3,4} {5,6}
# 環偵測:一條條加邊,若邊的兩端已在同組即出現環
g = UnionFind(4)
edges = [(0, 1), (1, 2), (2, 3), (0, 3)]
for u, v in edges:
if not g.union(u, v):
print(f"加入邊 ({u},{v}) 形成環!") # (0,3) 形成環
操作步驟#
- 初始化:每個元素自成一棵單節點樹,
parent[i] = i。 find(x):沿 parent 上溯到根;回程做路徑壓縮。union(x, y):找兩個根;同根則已是同組;否則比較秩,把秩小的根掛到秩大的根下,必要時秩加一。
複雜度分析#
| 實現 | find | union |
|---|---|---|
| 純樸(無優化) | O(n) 最壞 | O(n) |
| 僅路徑壓縮 | 攤攤 O(log n) | 攤攤 O(log n) |
| 壓縮 + 按秩合併 | 攤攤 O(α(n)) ≈ O(1) | 攤攤 O(α(n)) |
空間 O(n)。併查集的教訓值得銘記:若你只需要「等價類」資訊,就別用需要更多資訊的結構——簡化的表示法往往帶來更快的操作。
練習#
- 把「按秩合併」改成「按大小合併」(維護集合大小,小掛大),並加上
size(x)回傳 x 所在集合的人數。 - 統計實驗:對 n=10⁶ 做 10⁶ 次隨機 union,比較「無優化」「僅路徑壓縮」「兩者並用」三版的總耗時。
- n 個人中有若干好友關係,好友圈內可互相傳遞訊息。給定關係串列與查詢「a 能否傳訊給 b」,用併查集離線處理,說明為何比每次 BFS 快。