Kasaraju算法--強連通圖遍歷及其python實現

在理解有向圖和強連通份量前必須理解與其對應的兩個概念,連通圖(無向圖)和連通份量。算法

連通圖的定義是:若是一個圖中的任何一個節點能夠到達其餘節點,那麼它就是連通的。app

例如如下圖形:單元測試

這是最簡單的一個連通圖,即便它並不閉合。因爲節點間的路徑是沒有方向的,符合從任意一個節點出發,均可以到達其餘剩餘的節點這一條件,那麼它就是連通圖了。學習

 

連通份量測試

 

顯然這也是一個圖,只不過是由三個子圖組成而已,但這並不是一個連通圖。這三個子圖叫作這個圖的連通份量,連通份量的內部歸根仍是一個連通圖。spa

 

有向圖:code

在連通圖的基礎上增長了方向,兩個節點之間的路徑只能有單一的方向,即要麼從節點A連向節點B,要麼從節點B連向節點A。有向圖與連通圖(更準確來講是無向圖)最大的區別在於節點之間的路徑是否有方向。component

有向圖也分兩種,一種是有環路的有向圖。另一種是無環路的有向圖,即一般所說的有向無環圖DAG(Directed Acyclic Graph)。嚴格來講,第一種有環路的圖,若是任意一個節點均可以與其餘節點造成環路,那麼它也是一個連通圖。blog

 

例以下面的就爲一個有向圖同時也是連通圖:排序

 

強連通份量

強連通份量SCCs(strongly connected components)是一種有向的連通圖。

若是一個圖的連通份量是它裏面全部節點到可以彼此到達的最大子圖,那麼強連通份量SCCs就是一個有向圖中全部節點可以彼此到達的子圖。

 

顯然由345組成的子圖是沒法到達由012組成的子圖的。那麼012和345分別組成兩個強連通份量。

 

在實際的現實問題中,咱們考慮問題可能就不會簡單地研究無向圖。例如地圖上的最短路徑規劃,ARP路由算法等等,考慮的都是有向圖的問題。

若是有這樣一個需求,咱們但願用最少的次數遍歷全部節點,怎麼處理呢?

 

時間效應問題,強連通份量間的時間問題。

若是有向圖的各個強連通份量中的元素個數相仿,那麼,它們內部分別進行遍歷的時間量級別是相等的,但實際狀況是,這種狀況不多發生。通常從一個強連通份量到另外一個強連通份量。

 

正如上面的需求:如何用最少的次數遍歷整個有向圖的全部節點。假設咱們將0、一、2組成子圖1,將三、四、5組成子圖,子圖1有一條指向子圖2的路徑。這時候,咱們從子圖1的任意一點開始遍歷。假設咱們從1開始遍歷,那麼遍歷的順序將會是1—2,那麼來到2的時候問題來了,是先走0的路徑仍是走子圖1和子圖2之間的路徑去遍歷節點3呢?

若是咱們先遍歷節點0,那麼咱們遍歷完節點0以後,發現節點1已經遍歷過,就會返回節點2,再沿着子圖1和子圖2之間的路徑去遍歷子圖2。這看起來是挺合理的。

但問題是,若是是先遍歷節點3(也就是說先遍歷子圖2)呢?

假設沿着子圖1和子圖2的路徑去遍歷子圖2,那麼子圖2遍歷完後,子圖1還剩下節點0沒有被遍歷,這時候就會出現很爲難的事情,由於以前遍歷的狀況沒法判斷哪些節點是沒有遍歷的,只能是原路返回,依次去重新遍歷,「發現」哪些節點是還沒去遍歷的。彷佛上圖比較簡單,這種方法不會耗費太多的時間。但若是是節點2鏈接着(並指向)許多個強連通子圖的有向圖,這種「返回式」的遍歷將會是很費勁的一件事。

 

爲了解決這個問題,Kosaraju算法提出了它的解決方案。Kosaraju算法的核心操做是將全部節點間的路徑都翻轉過來。下面分析一下爲何這種算法有它的優點。

仍是拿上面的圖來說述。想象一下上面的有向圖中的全部節點間的路徑都翻轉過來了。讀者能夠本身用一張紙簡單畫一下。就像下面的圖:

 

這一次,咱們仍是以0、一、2組成子圖1,以三、四、5組成子圖2。所不一樣的是,此次遍歷的起始點從子圖1開始。

 

多強連通份量的有向圖

再來看一下這個多子圖的強連通圖,若是像上圖所示,從子圖1開始,就會像上文提到的那樣,遍歷到節點2,會出現多個去向的問題。而在尚未遍歷完子圖1的前提下,從節點2過渡到子圖2/子圖3,再回溯的時候會引來較大的麻煩。經過Kosaraju算法以後,從2節點出發的路徑都會變成指向2。此時,遍歷的起點仍是從子圖1開始,因爲子圖1沒有出路,就不會出現上面所說的問題。再遍歷完子圖1後,繼續遍歷子圖二、子圖3。而子圖二、子圖3的遍歷都是在強連通份量內部實現的。

 

 

算法實現

鄰接集表示的有向圖

N={
    "a":{"b"},   #a
    "b":{"c"},   #b
    "c":{"a","d","g"},   #c
    "d":{"e"},   #d
    "e":{"f"},   #e
    "f":{"d"},   #f
    "g":{"h"},   #g
    "h":{"i"},   #h
    "i":{"g"}    #i
}

 

翻轉圖實現代碼:

def re_tr(G):
    GT = {}
    for u in G:
        for v in G[u]:
            # print(GT)
            if GT.get(v):
                GT[v].add(u)
            else:
                GT[v] = set()
                GT[v].add(u)

    return GT

 

深度遍歷算法實現代碼:

#遞歸實現深度優先排序
def rec_dfs(G,s,S=None):
    if S is None:
        #S = set()    #集合存儲已經遍歷過的節點
        S = list()    #用列表能夠更方便查看遍歷的次序,而用集合能夠方便用difference求差集
    # S.add(s)
    S.append(s)
    print(S)
    for u in G[s]:
        if u in S:continue
        rec_dfs(G,u,S)

return S

 

在強連通圖內遍歷

#遍歷有向圖的強連通份量
def walk(G,start,S=set()):     #傳入的參數S,即上面的seen很關鍵,這避免了經過連通圖之間的路徑進行遍歷
    P,Q = dict(),set()      #list存放遍歷順序,set存放已經遍歷過的節點     
    P[start] = None
    Q.add(start)
    while Q:
        u = Q.pop()                      #選擇下一個遍歷節點(隨機性)
        for v in G[u].difference(P,S):         #返回差集
            Q.add(v)
            P[v] = u
    print(P)    
    return P

 

得到強連通份量

#得到各個強連通圖
def scc(G):
    GT = re_tr(G)
    sccs,seen = [],set()
    for u in rec_dfs(G,"a"):    #以a爲起點
        if u in seen:continue
        C = walk(GT,u,seen)
        seen.update(C)
        sccs.append(C)
    return sccs

 

單元測試

print(scc(N))

結果:
{'a': None, 'c': 'a', 'b': 'c'}
{'d': None, 'f': 'd', 'e': 'f'}
{'g': None, 'i': 'g', 'h': 'i'}
[{'a': None, 'c': 'a', 'b': 'c'}, {'d': None, 'f': 'd', 'e': 'f'}, {'g': None, 'i': 'g', 'h': 'i'}]

 

這是本人學習過程所寫的第一篇關於圖的算法文章,供你們一塊兒學習討論。其中不免會有錯誤。若有錯誤之處,請各位指出,萬分感謝!

相關文章
相關標籤/搜索