🎯 什麼情境該想到我

當你「要一直合併群組,並隨時問『這兩個在同一組嗎』」的時候——連通性、帳號去重合併、朋友圈、網路連通、Kruskal 最小生成樹。
★ 它是**少數「兩行優化把複雜度從 壓到幾乎 」**的資料結構。

⚙️ 怎麼用(機制與兩個優化)

結構:森林

每個集合是一棵樹,用「樹根」代表整個集合。

初始:  0  1  2  3  4          每個點自成一組

union(0,1), union(2,3):
        0     2    4
        |     |
        1     3

union(1,3):
          0        4
        /   \
       1     2
             |
             3
       find(3) = 0,find(1) = 0  → 同一組

基本實作(兩個優化缺一不可)

type DSU struct {
    parent []int
    rank   []int      // 樹高的上界(也可用 size)
    count  int        // ★ 目前的集合數量 —— 免費且常用
}
 
func NewDSU(n int) *DSU {
    p := make([]int, n)
    for i := range p { p[i] = i }
    return &DSU{parent: p, rank: make([]int, n), count: n}
}
 
// ★ 優化一:路徑壓縮
func (d *DSU) Find(x int) int {
    if d.parent[x] != x {
        d.parent[x] = d.Find(d.parent[x])   // 回程時把沿路所有節點直接掛到根上
    }
    return d.parent[x]
}
 
// ★ 優化二:按秩合併
func (d *DSU) Union(x, y int) bool {
    rx, ry := d.Find(x), d.Find(y)
    if rx == ry { return false }            // 已在同一組(Kruskal 用這個判斷成環)
    if d.rank[rx] < d.rank[ry] { rx, ry = ry, rx }
    d.parent[ry] = rx                       // ★ 矮的掛到高的下面
    if d.rank[rx] == d.rank[ry] { d.rank[rx]++ }
    d.count--
    return true
}

★ 兩個優化各自在做什麼

優化做什麼單獨使用的複雜度
按秩合併矮樹掛到高樹下,避免樹長成一條鏈
路徑壓縮Find 時把沿路的節點全部直接接到根上攤還
★ 兩者一起—攤還

為什麼「按秩合併」有效:如果總是把高樹掛到矮樹下,樹會越長越深。
反過來掛,可證明秩為 的樹至少有 個節點 → 樹高 。

為什麼「路徑壓縮」有效:每次 Find 都在攤平這棵樹。做過一次之後,
沿路所有節點的下一次 Find 都是 。

★ 是什麼(這是最值得記住的一點)

是 Ackermann 函數的反函數,成長速度慢到荒謬:

★ 也就是說:對任何「宇宙中可能存在的資料量」,。
實務上就當成常數。 併查集是實質 的。

(Tarjan 證明了 是緊界——不可能再更好。)

主要應用

應用做法
★ Kruskal 最小生成樹Union 回傳 false 就代表會成環,跳過這條邊
連通分量計數維護 count,每次成功 union 就減一
動態連通性「加邊之後 a 和 b 連通了嗎」—— ★ 比重跑 深度優先搜尋 快得多
★ 帳號/實體合併「這兩筆記錄是同一個人」→ union。實務上極常用(CRM 去重、ID 映射、爬蟲的實體解析)
等價關係任何「a 等價於 b」的傳遞閉包
★ 帶權併查集在 parent 邊上存「相對關係」(差值、比例),可以回答「a 比 b 大多少」——用於食物鏈、匯率、方程組一致性檢查

★ 它做不到什麼(重要的邊界)

做不到為什麼
★ 刪除/拆分(un-union)路徑壓縮破壞了歷史結構,無法還原。
→ 需要拆分就得用 link-cut tree 或離線處理(把刪除反轉成加入,倒著跑)
列出某個集合的所有成員只知道根,不知道成員。要另外維護一個 map[root][]member
有向圖的連通性併查集處理的是無向等價關係。有向圖的強連通分量要用 Tarjan(見 深度優先搜尋)

★ 「不能拆分」是最常被忽略的限制。 需求裡只要出現「取消合併」,併查集就不是答案。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • ★ 一定要兩個優化都做。只做一個是 ,兩個一起才是 。少寫一個不會出錯,只會慢——很難察覺。
  • Find 的遞迴在極端情況會很深(雖然路徑壓縮後很快變淺)。保守可以寫迭代版(兩趟:先找根、再壓縮)。
  • 不能拆分(見上)。
  • 按秩合併用 rank 或 size 都可以,但壓縮之後 rank 只是「高度的上界」不是真實高度——不要拿它當樹高用。
  • 並行環境需要額外設計(無鎖併查集存在但複雜)。
  • 只要靜態連通性就用 深度優先搜尋:一次 標記完所有分量,更簡單。併查集的價值在「動態加邊」。

🔗 相關工具