☘用Python來實現Disjoint Set (併查集/ Union-Find)

☘用Python來實現Disjoint Set (併查集/ Union-Find)

更新於 發佈於 閱讀時間約 11 分鐘

之前,已經學會了Linked List, 並且知道如何用Linked List來實作Queue 和 Stack。


今天,我們將用Python list來實現Disjoint Set (併查集,另外也有人稱之為Union-Find)。


Disjoint Set適合用於處理一些子集合的合併根節點的查找操作。
這種資料結構在圖論中非常有用,特別是在解決連通性相關問題的應用


定義

一個森林狀的資料結構。

一開始,每個ID對應到一顆樹,根節點都指向自己

整體看,就是有n顆樹(Tree)構成的森林(Forest)

raw-image

支援的操作與介面


DS.__init__(self, size)

初始化Disjoint Set,大小為參數size所指定


一開始,每個ID對應到一顆樹,根節點都指向自己

整體看,就是有n顆樹(Tree)構成的森林(Forest)

raw-image


DS.find(x)

尋找ID x所在的根節點(代表屬於哪個集合)


raw-image


DS.union(x, y)

合併ID x所在的集合 和 ID y所在的集合。

通常合併時,會以集合的size(也有課本稱之為rank)為依據。

size小的合併到size大的。

如果size相同,則合併到前者x所在的集合。


raw-image


raw-image



優點

1.使用path-compression之後,可以在 O (α(N)) ~ O(1) 常數時間內find()。

2.使用path-compression之後,可以在 O (α(N)) ~ O(1) 常數時間內union()。

缺點

1.相對不直覺,需要以陣列或字典去記錄每個ID的根結點。


Disjoint Set的class定義 與 建構子(初始化函式)

class DisjointSet:

def __init__(self, size):
self.parent = list(range(size))
self.rank = [1] * size

Disjoint Set常見的function實現


1.find(x)


尋找ID x所在的根節點(代表屬於哪個集合)。

並且沿路順便更新,把長輩節點的根結點都指向最上層的root。

這種技巧稱之為path compression。


時間複雜度: O (α(N)) ~ O(1) 常數時間

    def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # 路徑壓縮
return self.parent[x]

2.union(x, y)


合併ID x所在的集合 和 ID y所在的集合。

通常合併時,會以集合的size(也有課本稱之為rank)為依據。

size小的合併到size大的。

如果size相同,則合併到前者x所在的集合。


時間複雜度: O (α(N)) ~ O(1) 常數時間

    def union(self, x, q):
rootX = self.find(x)
rootY = self.find(q)

if rootX != rootY:
# 按size (rank)合併
if self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
elif self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1

完整的Disjoint Set實作和程式碼,
底層是Python dictionary。


class DisjointSet:
def __init__(self, size):
self.parent = list(range(size))
self.rank = [1] * size

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, q):
rootX = self.find(x)
rootY = self.find(q)

if rootX != rootY:
# 按size (rank)合併
if self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
elif self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1

def test():
# Example

# ID = 0, 1, 2, ..., 9
ds = DisjointSet(10)

print("After initialization")
for i in range(10):
print(f"root of {i} = {ds.find(i)}")

ds.union(1, 2)

print("\nAfter union(1, 2)")
for i in range(10):
print(f"root of {i} = {ds.find(i)}")

ds.union(3, 4)

print("\nAfter union(1, 2) and union(3, 4)")
for i in range(10):
print(f"root of {i} = {ds.find(i)}")

ds.union(1, 3)
print("\nAfter union(1, 2), union(3, 4), union(1, 3)")
for i in range(10):
print(f"root of {i} = {ds.find(i)}")

if __name__ == '__main__':
test()

測試輸出

After initialization
root of 0 = 0
root of 1 = 1
root of 2 = 2
root of 3 = 3
root of 4 = 4
root of 5 = 5
root of 6 = 6
root of 7 = 7
root of 8 = 8
root of 9 = 9

After union(1, 2)
root of 0 = 0
root of 1 = 1
root of 2 = 1
root of 3 = 3
root of 4 = 4
root of 5 = 5
root of 6 = 6
root of 7 = 7
root of 8 = 8
root of 9 = 9

After union(1, 2) and union(3, 4)
root of 0 = 0
root of 1 = 1
root of 2 = 1
root of 3 = 3
root of 4 = 3
root of 5 = 5
root of 6 = 6
root of 7 = 7
root of 8 = 8
root of 9 = 9

After union(1, 2), union(3, 4), union(1, 3)
root of 0 = 0
root of 1 = 1
root of 2 = 1
root of 3 = 1
root of 4 = 1
root of 5 = 5
root of 6 = 6
root of 7 = 7
root of 8 = 8
root of 9 = 9

結語


Disjoint Set其實就是對應到數學中的集合、集合合併的操作

讀著可以試著舉幾個小範例,用紙筆追蹤驗算法來驗證輸出結果,是否和預期相符合。



Disjoin Set相關的演算法練習題與詳解


🗿字典應用: Most Stones Removed with Same Row or Column_LC#947

avatar-img
小松鼠的演算法樂園
95會員
425內容數
由有業界實戰經驗的演算法工程師, 手把手教你建立解題的框架, 一步步寫出高效、清晰易懂的解題答案。 著重在讓讀者啟發思考、理解演算法,熟悉常見的演算法模板。 深入淺出地介紹題目背後所使用的演算法意義,融會貫通演算法與資料結構的應用。 在幾個經典的題目融入一道題目的多種解法,或者同一招解不同的題目,擴展廣度,並加深印象。
留言
avatar-img
留言分享你的想法!
在之前的教學中,已經學會了用雙向鏈結串列來實作Stack 堆疊。 今天,要用另一種底層資列結構,python list,來實作Stack 堆疊。 讀者可以從中發現,因為python list的功能和function實作已經很豐富, 所以使用起來,相當直覺,也簡單許多。
在這次的BMI(身體質量指標)計算的續集裡,將學會funciton的基本觀念與實作, 把常用的功能包裝成可重複利用的元件: function。
在之前的教學中,已經學會了Node和Linked List的實作, 用Python實現了單向鏈結串列Singly linked list、雙向鏈結串列Doubly linked list。 今天要承接之前打下的基礎,用雙向鏈結串列來實作Queue(佇列 或稱 隊列)。
在之前的教學中,已經學會了Node和Linked List的實作, 用Python實現了單向鏈結串列Singly linked list、雙向鏈結串列Doubly linked list。 今天要承接之前打下的基礎,用雙向鏈結串列來實作Stack 堆疊。
在資料結構與演算法裡, 最簡單的線性資料結構除了array之外就是linked list鏈結串列了。 Linked list又有分為單向Singly linked list 和雙向Doubly linked list
在這次的BMI(身體質量指標)計算教學裡, 將學會如何用python來接收使用者輸入的訊息,並且做一些簡單的四則運算。
在之前的教學中,已經學會了用雙向鏈結串列來實作Stack 堆疊。 今天,要用另一種底層資列結構,python list,來實作Stack 堆疊。 讀者可以從中發現,因為python list的功能和function實作已經很豐富, 所以使用起來,相當直覺,也簡單許多。
在這次的BMI(身體質量指標)計算的續集裡,將學會funciton的基本觀念與實作, 把常用的功能包裝成可重複利用的元件: function。
在之前的教學中,已經學會了Node和Linked List的實作, 用Python實現了單向鏈結串列Singly linked list、雙向鏈結串列Doubly linked list。 今天要承接之前打下的基礎,用雙向鏈結串列來實作Queue(佇列 或稱 隊列)。
在之前的教學中,已經學會了Node和Linked List的實作, 用Python實現了單向鏈結串列Singly linked list、雙向鏈結串列Doubly linked list。 今天要承接之前打下的基礎,用雙向鏈結串列來實作Stack 堆疊。
在資料結構與演算法裡, 最簡單的線性資料結構除了array之外就是linked list鏈結串列了。 Linked list又有分為單向Singly linked list 和雙向Doubly linked list
在這次的BMI(身體質量指標)計算教學裡, 將學會如何用python來接收使用者輸入的訊息,並且做一些簡單的四則運算。