Union-Find data structure 又稱 Disjoint-set data structure,用於處理不相交集合 (disjoint set) 的合併 (Union) 與查詢 (Find) 問題。

# Disjoint Set

Disjoint set 表示數個 set 之間,擁有的元素都不相同,彼此互斥 (disjoint)

可以去看看這篇文章 - disjoint set

# Union Find Algorithm

Union-Find Data Structure 是一種 forest 結構,forest 是一種 N-way Tree 結構,互相連通的節點放在同一個 set,任意選擇其中一個節點作為 root。

Union Find 提供以下兩種操作:

  • Find: 找到 input 節點的 root,可以藉此確定 input 節點屬於哪個子集。

    • Find 函數能夠找到節點的 root。
    • 如果要確認兩個節點是否為同一子集,只要分別找兩個節點的 root ,如果一樣,即屬於同一子集 (為 connected component)。
    • Find 的時間複雜度最差就是遍歷整棵樹,時間複雜度為 O (n)。
  • Union: 將兩個子集合併為同一子集。

    • Union 能夠將兩個所屬的子集進行合併。
    • 最簡單的方法就是將一個子集的 root 直接作為另一子集 root 的子節點即可

    • Union 的實踐需要依靠 find,因此時間複雜度最差為 O (n)。

以上圖的兩個樹來說,以 0 為 root 的樹大於以 2 為 root 的樹,如果將後者合併到前者下,合併過後的樹高度會比較小,根據這個觀察可以歸納出,如果兩個子集要合併,應該讓高度較小的子集合併到高度比較大的子集下,可以避免樹的不平衡。

# Find with Path Compression

Path compression 是一個優化技巧,也有人稱為 set collapsing, 讓每個節點直接連到它的 root 節點,這樣 Find 跟 Union 操作的時間複雜度可以降到 O (1)。

但要如何有效地讓所有節點 parent node 都指向 root 呢?
答案就是透過遞迴,透過遞迴找到 root, 再依序回傳更新為每個走訪過的 parent。

int find(int x) {
    if (x == root[x]) return x;
    return root[x] = find(root[x]);
}

# Union by Size/Rank

應用 path compression 後的樹可以盡可能縮減樹的高度 ,若要再 Union 兩個壓縮後的子集,可以採取 union by size 技巧,將子節點比較少的 root 加入比較大的子集。

依照 rank 來排序,起初每個點的 rank 均為 0 ,依據 rank 大小來決定如何合併,rank 大的子集合併小的,合併別人的子集其 rank 往上增加。

void unionSet(int x, int y) {
    int root_x = find(x);
    int root_y = find(y);
    if (root_x == root_y) return ;
    if (rank[root_x] > rank[root_y]) root[root_y] = root_x;
    else if (rank[root_x] < rank[root_y]) root[root_x] = root_y;
    else {
        root[root_y] = root_x;
        rank[root_x]++;
    }  
}

一開始每個點的 rank 都為 0 ,有一點特別容易寫出 bug ,在比較彼此 rank 或是更新 rank 時候,需要以 find() 所找出的 root 來比,不是各個節點本身的 rank