拓撲排序的Kahn算法和DFS的深搜


DAG圖和拓撲排序(Topological sorting)

一個無環的有向圖稱為有向無環圖(DAG)。圖的頂點可以表示要執行的任務,並且邊可以表示一個任務必須在另一個之前執行的約束; 在這個應用程序中,拓撲排序只是任務的有效序列。 當且僅當圖形沒有有向循環時,即如果它是有向無環圖(DAG),則可以進行拓撲排序。 任何DAG都具有至少一個拓撲排序。

在計算機科學領域,有向圖的拓撲排序或拓撲排序是其頂點的線性排序。在圖中假設從頂點 i 到頂點 j 中有一條有向路徑 i -> j。那么我們稱 i 為 j 的前驅,稱 j 為 i 的后繼。

定義:

對於一個DAG(有向無環圖)𝐺,將 𝐺 中所有頂點排序為一個線性序列,使得圖中任意一對頂點 𝑢 和 𝑣,若 𝑢 和 𝑣 之間存在一條從 𝑢 指向 𝑣 的邊,那么 𝑢 在線性序列中一定在 𝑣 前。

注意:DAG的拓撲序可能並不唯一。只要滿足對於(u->v),在線性序列中,u在v前面即可

用途:

  • 判環
  • 判鏈
  • 處理依賴性任務規划問題
  • 用在項目管理、 數據庫查詢優化和矩陣乘法的次序優化上。

 

Kahn算法

拓撲排序的算法非常簡單,常用的方式為Kahn’s algorithm算法,Kahn算法有時候也叫做toposort的bfs版本。基本步驟為:

  • 入度為 0 的點組成一個集合 𝑆
  • 從 𝑆 中取出一個頂點 𝑢插入拓撲序列
  • 遍歷頂點 𝑢 的所有出邊,並全部刪除,如果刪除這條邊后對方的點入度為 0,也就是沒刪前,𝑢𝑣 這條邊已經是 𝑣 的最后一條入邊,那么就把 𝑣 插入 𝑆
  • 重復執行上兩個操作,直到 𝑆=。此時檢查拓撲序列是否正好有 𝑛 個節點,不多不少。如果拓撲序列中的節點數比 𝑛 少,說明 𝐺 非DAG,無拓撲序,返回false。如果拓撲序列中恰好有 𝑛 個節點,說明 𝐺 是DAG,返回拓撲序列。

也就是說,Kahn算法的核心就是維護一個入度為0的頂點

代碼如下:

 1 def topoSort(graph, ind):
 2     topo = []
 3     queue = []
 4 
 5     for i in range(len(ind)):
 6         if ind[i] == 0:
 7             queue.append(i)
 8 
 9     while queue:
10         node = queue.pop(0)
11         topo.append(node)
12         for i in range(len(graph[node])):
13             v = graph[node][i]
14             ind[v] -= 1
15             if ind[v] == 0:
16                 queue.append(v)
17 
18     if len(topo) == len(ind):
19         print(topo)
20         return True
21     return False
22 
23 
24 if __name__ == '__main__':
25     data = [
26         [0, 1],
27         [0, 2],
28         [1, 2],
29         [1, 3],
30         [2, 3],
31         [2, 5],
32         [3, 4],
33         [7, 6],
34     ]
35     n = 8
36     ind = [0 for _ in range(n)]
37     graph = [[] for _ in range(n)]
38     for u, v in data:
39         graph[u].append(v)
40         ind[v] += 1
41 
42     topoSort(graph, ind)

 

利用DFS實現拓撲排序

當一個有向圖無環的時候,我們可以利用DFS算法來實現拓撲排序。原理很簡單,由於圖中沒有環,那么由圖中某點出發的時候,最先退出DFS的頂點一定是出度為0的頂點,也就是拓撲排序中最后的一個頂點(逆向思維)。因此按DFS退出的先后記錄下的頂點序列就是逆向的拓撲排序的序列。

從任意一個未被訪問的結點出發做深搜后序遍歷。遍歷所有結點,回溯前記錄結點,最后路徑再倒序一下就是正確的拓撲排序(或者建圖的時候就把邊的方向倒了,最后得到的排序不用倒)。如果有多個子圖,要多次深搜,直到所有結點都被訪問完(所有子圖都搜完)得到多個子序列,再拼接一起就是答案。

代碼如下:

 1 def dfs(node, graph, vis, order):
 2     vis[node] = True
 3     for n in graph[node]:
 4         if not vis[n]:
 5             dfs(n, graph, vis, order)
 6     order.append(node)
 7 
 8 
 9 def topoDfsSort(graph):
10     order = []
11     vis = [False for _ in range(n)]
12     for i in range(n):
13         if not vis[i]:
14             dfs(i, graph, vis, order)
15     print(order[::-1])
16 
17 
18 if __name__ == '__main__':
19     data = [
20         [0, 1],
21         [0, 2],
22         [1, 2],
23         [1, 3],
24         [2, 3],
25         [2, 5],
26         [3, 4],
27         [7, 6],
28     ]
29     n = 8
30     ind = [0 for _ in range(n)]
31     graph = [[] for _ in range(n)]
32     for u, v in data:
33         graph[u].append(v)
34         ind[v] += 1
35 
36     topoDfsSort(graph)

 

對於DFS方法有人可能疑惑為什么要回溯前才記錄(要用后序遍歷的原因),而且為什么可以從任一點開始?

  • 能回溯的點說明已經把子代遍歷完,確定是最后的了,於是可以記錄下來,得到一個倒序記錄。拓撲排序的一個節點可能有多個父結點,所以無法確定某點為先。如下圖,如果我已遍歷得0–>2順序,但是在2之前還有一個點1,明顯不對,先序遍歷行不通。這是由結構所決定的,不同於樹,拓撲排序分支節點之間可能有聯系的,導致一個節點可能有多個父結點,故沒有嚴格的先后順序。而從后往前記錄,因為節點遍歷完分支,故不怕節點再指向任何節點也就不可能指回前面節點,后序遍歷可行。如果要對一個二叉樹做拓撲排序,代碼與此大同小異,二叉樹只有兩個子結點,無需for循環而已。
  • 從任意點開始遍歷都行,是因為后序遍歷保證了已記錄的就是最靠后的了。后面記錄的結點肯定不會后於已記錄的結點。比如先從3開始搜,3->4(記錄下4->3),然后無論從哪個結點再開始搜,都不會打亂順序了,012都是先於3(可能的記錄下為4->3->5->2->1->0->6->7),567放3前面也沒事(可能的記錄為4->3->6->7->5->2->1->0)。

 

拓撲排序實現的時間復雜度

Kahn算法和dfs算法的時間復雜度都為 O(𝐸+𝑉)

另外,如果要求字典序最小或最大的拓撲序,只需要將Kahn算法中的q隊列替換為優先隊列即可,總時間復雜度為 O(𝐸+𝑉log𝑉)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM