多用戶合併 利用 並查集——求無向圖的所有連通子圖

並查集——求無向圖的所有連通子圖

求解無向圖的連通子圖,有兩種方法,一種是DFS或BFS,也就是對圖遍歷,另一種方法就是使用並查集。對圖的遍歷非常常見,而並查集的概念就不如遍歷那麼熟悉。其實如果僅是找連通子圖,用DFS對所有節點遍歷一遍就可以,而用並查集則需要遍歷兩遍。我們不考慮算法效率問題,僅僅是通過這個問題讓我們對並查集有所認識,並瞭解其原理,下面主要說一下並查集。
  首先說一下,並查集是一種設計非常好的數據結構,也是一種檢索算法。說它是數據結構,因爲使用並查集的最終結果是生成一個森林,裏面包括一個或多個樹。說它是一種算法,是因爲通過並查集,我們很容易判斷圖中任意兩個節點是否具有連通性,同時也可以求解出所有連通子圖,也就是即將說到的內容。

問題分析
  現在來看問題:求無向圖的所有連通子圖,可以分解成兩步,首先,將所有連通的節點,都放到一起,最終分成幾個連通分量組;然後,找出屬於同一連通分量組的節點及邊,也就是各個連通子圖。
  第二步非常容易,假設第一步生成一個字典,裏面存放着所有節點所對應的連通分量組號,那麼再對原有的圖遍歷一遍,從字典中查出節點所屬的組並且根據組進行區分,就可以得到所有連通子圖。查字典的複雜度是O(1),沒有什麼計算開銷。那麼問題主要變爲第一步中該如何生成那個字典。
  在這裏插入圖片描述
看上面這4個點,c1、c2有連接,我們需要在字典裏建立兩個值,{c1:Gc},{c2:Gc},Gc代表他們的連通分量組號,這樣通過分別查找c1的組號,c2的組號,所得結果一樣,我們可以判斷出c1、c2屬於同一組,具有連通性。同樣的,我們在字典中又加入了兩個值{c3:Gc},{c4:Gc},我們可以很easy的知道,c3和c4屬於同一個連通分量,具有連通性,雖然它們之間沒有直接的邊相連。對於b開頭的3個點,我們在字典中加入它們的組號{b1:Gb},{b2:Gb},{b3:Gb}。根據字典,我們知道,c3和b3不屬於同一組,它們之間不連通。
  那麼問題來了,我們怎麼設置字典中的那個組號呢?我認爲這就是並查集的精髓所在。
實現過程
  1、把節點編號當做組號。
  因爲要建立字典,我們需要對整個圖掃一遍,使字典中的內容覆蓋到每一個節點。
  上面的那個組號Gc和Gb是人爲造的,實際中我們需要按照規則設定組號。
  這個規則的基本思想就是,選擇節點編號當做組號:
  a) 因爲一條邊有兩個節點,選擇一條邊任意一個節點編號當做這條邊上兩個節點的組號;
  b) 如果字典中已經有了節點的組號,那麼選擇字典中的,如果沒有,則按照上面規則選擇節點組號;
  對於上述的圖,我們按照(c1,c2)–>(c1,c4)–>(c2,c3)–>(b1,b2)–>(b2,b3)的順序掃這個圖。按照上面的規則,假設都選擇小編號作爲組號,
  掃到第1條邊(c1,c2)時,建立字典 {c1:c1, c2:c1},
  掃到第2條邊(c1,c4)時,建立字典 {c1:c1, c2:c1, c4:c1},
  掃到第3條邊(c2,c3)時,建立字典 {c1:c1, c2:c1, c3:c2, c4:c1},
  掃到第4條邊(b1,b2)時,建立字典 {c1:c1, c2:c1, c3:c2, c4:c1, b1:b1, b2:b1},
  掃到第5條邊(b2,b3)時,建立字典 {c1:c1, c2:c1, c3:c2, c4:c1, b1:b1, b2:b1, b3:b2}。
  2、找組號的組號,直到找到祖先。
  在第一步中我們初步建立了一個字典,裏面包括每一個節點。可以看到,節點c3的組號爲c2,和節點c1、c2、c4不一致。按照當前的結果,c3被認定爲與其它3個c節點都不連通。所以目前還沒有完成分組。爲了解決這個問題,當我們再次遍歷字典時,需要對每個節點得到的組號再次進行尋找組號的操作,直到得到的組號是它自己。對應的代碼就是:

def find(key):
    while parent[key] != key:  //parent就是得到的那個字典
        key = parent[key]
    return key

代碼非常簡短,對於c3來說,從字典中得到組號是c2,和c3不等,那就繼續找c2的組號,得到c1,和c2還不等,那就找c1的組號,這次得到c1,和自身相等,返回c1,也就是c3的組號。經過不斷的查找,終於c3和其它c節點擁有了相同的組號,被劃分爲了一組。
  3、壓縮路徑
  從第2步中看到,爲了找c3的組號,有點費勁啊,先找到c2,再找到c1,如果下次還想找c3的組號,還需要這麼折騰一次。能不能一下就給出c3的組號是c1呢?沒問題,當我們用find方法找c3的時候,得到的組號不是自己的編號,那麼我就知道c3的組號一定是它的組號c2的組號,那麼我們就把c3的組號直接設定爲c2的組號就可以了。只需要在find中改一行就OK了。

def find(key):
    while parent[key] != key:  #parent就是得到的那個字典
        parent[key] = parent[parent[key]]  #把c3的組號設定爲c2的組號
        key = parent[key]
    return key

4、合併家族
  比如我們的圖又擴大了,在原有基礎之上有加了幾個節點。如圖:
  合併家族
  新加了兩條邊,(c5, c6),(c5, c1),按照這個順序,當掃完(c5, c6)時,會在字典中加入{c5:c5, c6:c5}兩個值。再掃到(c5, c1)時,由於c5有自己的組號,c1也有自己的組號,各自爲營,但它倆又有連接。一山不容二虎,那就選一個當老大。在這裏就是隨便選一個作爲另一個的組號。咦,這個怎麼又回到的1的問題。。。對的,其實1和4是一個問題,只是1是初始化時的選擇方式,4是遍歷到中途過程中的選擇方式,它倆合併到一起的代碼就是:

def init(key):
    if key not in parent:
        parent[key] = key

def join(key1, key2):
    p1 = find(key1)
    p2 = find(key2)
    if p1 != p2:
        parent[p2] = p1

當我們對圖的邊進行遍歷時,就先執行init,看節點是否有組號,沒有組號就賦值爲自己。再執行join,合併邊上的兩個節點爲同一個組。
  假設選c1的組號作爲c5的組號,那麼目前的字典內容就如下圖所示:
  {c1:c1, c2:c1, c3:c1, c4:c1, c5:c1, c6:c5, b1:b1, b2:b1, b3:b2}。
  對應的數據結構就是:
數據結構
這就是我們最終得到的結果,一個森林,裏面包括了兩棵樹,每棵樹代表連通的節點組,樹中節點的父節點代表自己的組號。
  根據森林中的樹,我們就可以找到圖中的各個連通子圖了,當然,還需要根據這個森林中的內容,也就是我們得到的字典,再遍歷一遍圖,找到各個連通子圖的邊,不過已經完美解決了需要的問題。
  再回過頭來看,其實代碼非常的簡短,就三個函數,完整代碼如下:

完整代碼

class UnionSet(object):
    def __init__(self):
        self.parent = {}

    def init(self, key):
        if key not in parent:
            self.parent[key] = key

    def find(self, key):
        self.init(key)
        while self.parent[key] != key:  
            self.parent[key] = self.parent[self.parent[key]] 
            key = self.parent[key]
        return key      

    def join(self, key1, key2):
        p1 = self.find(key1)
        p2 = self.find(key2)
        if p1 != p2:
            self.parent[p2] = p1

應用

除了求圖的連通子圖(連通分量)可以用到並查集外,在用Kruskal方法求圖的最小生成樹,也用到了並查集,掌握了並查集,那麼再去看Kruskal的方法,就會輕而易舉了。
  轉載自https://blog.csdn.net/wangyibo0201/article/details/51998273?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章