【算法】圖論(一) —— 基本圖算法(BFS/DFS/強連通分量)

基本圖算法

一、圖的表示

對於圖G=(V,E),可以用兩種表示方法表示,一種將圖表示爲鄰接鏈表,另一種將圖表示爲鄰接矩陣。這兩種方法都既可以表示無向圖,又可以表示有向圖。
1. 鄰接鏈表
鄰接鏈表適用於稀疏圖(邊的條數|E| 遠小於|V|2 的圖)。鄰接鏈表由一個包含|V|條鏈表的數組Adj構成,每個節點有一條鏈表。若G是無向圖,則Adj[u]包含所有與u鄰接的節點,所有鄰接鏈表之和爲2|E|;若G是有向圖,則Adj[u]包含節點u所指向的所有節點,所有鄰接鏈表之和爲|E|。無論是有向圖還是無向圖,鄰接鏈表表示法的存儲空間需求爲Θ(V+E) 。鄰接鏈表的一個潛在缺陷是無法快速判斷一條邊(u,v)是否在圖中,唯一的方法是在鄰接鏈表Adj[u]裏搜索節點v。我們可以通過用散列表來替代鏈表加快邊的搜索,但是缺陷是散列表的大小及散列函數難以確定,而且這種方法只在每條邊的查詢頻率相同時效率最大。鄰接矩陣也克服了這個缺陷,但需要更大的存儲空間消耗。
2. 鄰接矩陣
鄰接矩陣對圖G中的節點任意編號1,2,…,|V|,在編號後,用一個|V|×|V| 的矩陣A=(aij) 予以表示,該矩陣滿足下述條件:

aij={10(i,j)E

使用鄰接矩陣表示一個圖,無論圖中邊的數量爲多少,其所需存儲空間均爲Θ(V2) 。對於無向圖,其鄰接矩陣爲一個對稱矩陣,所以在某些應用中只需要存儲對角線及其以上這部分鄰接矩陣即可,從而將鄰接矩陣的存儲空間需求減少幾乎一半。
3. 兩種表示方法的比較
鄰接矩陣表示比較簡單,所以在圖規模比較小的時候,可能更傾向於使用鄰接矩陣表示,而且對於無向圖來說,鄰接矩陣的一個優勢是每個記錄項只需要一位空間。

二、廣度優先搜索(BFS)

通常用於尋找特定源節點出發的最短路徑距離,所以圖通常爲連通圖。

廣度優先搜索是最簡單的圖算法之一,是許多圖算法模型的原型(Prim/Dijkstra等)。給定圖G=(V,E)和一個可以識別的源節點s,廣度優先搜索對圖G中的邊進行系統性的探索來發現可以從源節點s到達的所有節點。該算法能夠計算從源節點s到每個可到達節點的距離(最少的邊數),同時生成一棵“廣度優先搜索樹”。該樹以源節點s爲根節點,包含所有可以從s到達的節點。對於每個從源節點s可以到達的節點v,在廣度優先搜索樹裏從節點s到節點v的簡單路徑所對應的就是圖G中從節點s到節點v的“最短路徑”,即包含最少邊數的路徑。該算法對有向圖和無向圖同樣適用。
在執行廣度優先搜索的過程中將構造出一棵廣度優先樹。一開始該樹只有根節點,即源節點s。在掃描已發現節點u的鄰接鏈表時,每當發現一個未被發現的節點v,就將節點v和邊(u,v)同時加入該樹。以下爲算法導論中的BFS僞代碼,代碼中爲每個節點設置一個color屬性,白色節點爲未被發現節點,黑色節點和灰色節點均爲發現節點,區別在於黑色節點的所有鄰居節點都已經被發現,而灰色節點存在部分白色鄰居節點。

BFS(G,s)
    // 初始化圖中除源節點s外的所有節點屬性
    for each vertex u in G.V-{s}
        u.color = WHITE // 未被發現
        u.d = INF       // 與源節點的距離爲無限大
        u.pi = NIL      // 前驅節點/父節點爲空
    // 初始源節點s屬性
    s.color = GRAY
    s.d = 0
    s.pi = NIL
    Q = empty set      // 初始化灰色節點集
    ENQUEUE(Q, s)
    while Q is NOT an empty set
        u = DEQUEUE(Q)
        for each v in G.Adj[u]
            if v.color == WHILE
                v.color = GRAY
                v.d = u.d+1
                v.pi = u
                ENQUEUE(Q, v)
        u.color = BLACK 

算法複雜度分析:
每個節點的入隊操作和出隊操作最多均爲1次,入隊和出隊的時間均爲O(1),因此對隊列操作的總時間爲O(V)。因爲算法只在一個節點出隊時纔對該節點的鄰接鏈表進行掃描,所以每個鄰接鏈表最多隻掃描一次。由於所有鄰接鏈表的長度之和爲Θ(E) ,用於掃描鄰接鏈表的總時間爲O(E)。初始化操作的成本爲O(V),因此廣度優先搜索的總時間爲O(V+E),即廣度優先搜索的運行時間是圖G的鄰接鏈表大小的一個線性函數。

三、深度優先搜索(DFS)

通常作爲另一個算法中的子程序,所以也常常用於不是連通圖的圖中。

深度優先搜索總是探索最近發現節點的子節點,知道探索到不存在子節點的節點v,則“回溯”到v的父節點,該過程一直持續到從源節點可以到達的所有節點都被發現爲止。若還存在未發現節點,則從未發現節點任選一個作爲新的源節點,重複同樣過程,直到所有節點都被發現。
類似廣度優先搜索算法,深度優先搜索在算法導論中同樣使用顏色屬性來指明節點狀態,初始爲白色,節點被發現爲灰色,節點的鄰接鏈表被掃描完成爲黑色。同時,深搜中每個節點有兩個時間戳:第一個時間戳v.d記錄節點v第一次被發現的時間(塗上灰色的時候);第二個時間戳v.f記錄搜索完成對v的鄰接鏈表掃描時間(塗上黑色的時候)。時間戳提供了圖結構的重要信息,通常能夠幫助推斷深度優先搜索算法的行爲。以下僞代碼給出基本的深度優先算法。

DFS(G)
    for each vertex u in G.V
        u.color = WHITE
        u.pi = NIL
    time = 0  // 全局變量,用於計算時間戳
    for each vertex u in G.V
        if u.color == WHITE
            DFS-VISIT(G, u)
DFS-VISIT(G, u)
    time = time+1 //白色節點u剛剛被發現
    u.d = time
    u.color = GRAY
    for each v in G.Adj[u] // 探索邊(u,v)
        if v.color = WHITE
            v.pi = u
            DFS-VISIT(G, v)
    u.color = BLACK
    time = time+1
    u.f = time

算法複雜度分析:
對於DFS(G)函數,排除對DFS-VISIT(G,u)的調用,其所需時間爲Θ(V) 。對於每一個節點vV ,DFS-VISIT函數的調用次數爲一次(當且僅當該節點爲白色時,DFS-VISIT中將調用該函數的白色節點塗成灰色)。DFS-VISIT函數中遍歷節點的鄰接鏈表,對於所有節點來說,其操作成本爲Θ(E) ,因此深搜的運行時間爲Θ(V+E)
深搜中,節點的發現時間和完成時間具有括號化結構(parenthesis structure),則發現時間和完成時間的歷史記載形成規整的表達式,即所有括號都是正確的嵌套在一起,通過節點的兩個時間戳,可以確定兩個節點之間的關係(後代關係)。

四、強連通分量

強連通分量:對於有向圖G=(V,E),強連通分量是一個最大節點集合CV ,對於該集合中的任一節點對u和v,路徑u->v和路徑v->u同時存在,即節點u和節點v可以互相到達。
強連通分量是深度優先搜索的一個經典應用,許多針對有向圖的算法都以此種分解操作開始。
尋找強連通分量需要對圖G=(V,E)進行轉置得到GT=(V,ET) ,其中ET={(u,v):(v,u)E} ,也就是對圖G中的所有邊進行反向得到。給定圖G的鄰接鏈表,得到其轉置所需的時間爲O(V+E)。可以看出圖G 和圖GT 中的強連通分量完全相同。算法導論中給出一個線性時間(Θ(V+E) 時間)算法,通過兩次深搜來計算有向圖中的強連通分量。

STRONGLY-CONNECTED-COMPONENTS(G)
    call DFS(G) t compute finishing time u.f for each vertex u
    compute G^T
    call DFS(G^T), but in the main loop of DFS, consider the vertices in order of decreasing u.f (as computed above)
    output the vertices of each tree in the depth-first forest formed as a separate strongly connected component

該算法能夠正確工作的關鍵在於:在轉置圖GT 中,連接不同強連通分量的每條邊都是從完成時間較早(第一次深搜所計算的完成時間)的分量指向完成時間較遲的分量,如下圖所示,第一次深搜會構建一棵深度優先樹,每個節點中記錄該節點的發現時間/完成時間:

參考代碼

[1] 廣度優先搜索算法/深度優先搜索算法/強連通分量算法

發佈了43 篇原創文章 · 獲贊 12 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章