1. 程式人生 > >有向無環圖DAG 拓撲排序 程式碼解釋

有向無環圖DAG 拓撲排序 程式碼解釋

目錄:

  1. DAG定義
  2. 舉例描述
  3. 實際運用
  4. 演算法描述
  5. 演算法實戰
  6. 演算法視覺化

定義

在圖論中,由一個有向無環圖的頂點組成的序列,當且僅當滿足下列條件時,稱為該圖的一個拓撲排序(英語:Topological sorting)。

  • 每個頂點出現且只出現一次;
  • 若A在序列中排在B的前面,則在圖中不存在從B到A的路徑。

也可以定義為:拓撲排序是對有向無環圖的頂點的一種排序,它使得如果存在一條從頂點A到頂點B的路徑,那麼在排序中B出現在A的後面。

或者

有向無環圖(Directed Acyclic Graph, DAG)是有向圖的一種,字面意思的理解就是圖中沒有環。常常被用來表示事件之間的驅動依賴關係,管理任務之間的排程。拓撲排序是對DAG的頂點進行排序,使得對每一條有向邊(u, v),均有u(在排序記錄中)比v先出現。亦可理解為對某點v而言,只有當v的所有源點均出現了,v才能出現。

需要記住的英語單詞:

E: edge 邊 V: vertex 頂點

常會看到(E, V)描述

舉例描述

有向無環圖的拓撲排序:

正面例子

左側是有向無環圖,右側是拓撲排序後的佇列。在右側佇列中,沿著箭頭方向的任意線性順序在左側圖中都有對應的路線,此時,我們才能說右側的拓撲排序是成功的排序。

注意:一個圖可以有很多種或者沒有一種拓撲排序序列 (n >= 0)

反面例子

非法拓撲排序,因為D比E後出現了。

強調:拓撲排序後的序列要保證所有箭頭方向路線在圖中都有對應路線才能說拓撲排序正確

有向樹,有向無環圖,無向圖對比

實際運用

拓撲排序 Topological Sorting 是一個經典的圖論問題。他實際的運用中,拓撲排序可以做如下的一些事情:

  • 檢測編譯時的迴圈依賴
  • 制定有依賴關係的任務的執行順序

拓撲排序不是一種排序演算法

雖然名字裡有 Sorting,但是相比起我們熟知的 Bubble Sort, Quick Sort 等演算法,Topological Sorting 並不是一種嚴格意義上的 Sorting Algorithm。

確切的說,一張圖的拓撲序列可以有很多個,也可能沒有。拓撲排序只需要找到其中一個序列,無需找到所有序列。

演算法描述:

入度與出度

在介紹演算法之前,我們先介紹圖論中的一個基本概念,入度

出度,英文為 in-degree & out-degree。 在有向圖中,如果存在一條有向邊 A-->B,那麼我們認為這條邊為 A 增加了一個出度,為 B 增加了一個入度。

演算法流程

拓撲排序的演算法是典型的寬度優先搜尋演算法,其大致流程如下:

  1. 統計所有點的入度,並初始化拓撲序列為空。
  2. 將所有入度為 0 的點,也就是那些沒有任何依賴的點,放到寬度優先搜尋的佇列中
  3. 將佇列中的點一個一個的釋放出來,放到拓撲序列中,每次釋放出某個點 A 的時候,就訪問 A 的相鄰點(所有A指向的點),並把這些點的入度減去 1。
  4. 如果發現某個點的入度被減去 1 之後變成了 0,則放入佇列中。
  5. 直到佇列為空時,演算法結束,

演算法實戰:

用lintcode真題實戰鞏固此演算法:

題目描述:

615. 課程表 現在你總共有 n 門課需要選,記為 0 到 n - 1. 一些課程在修之前需要先修另外的一些課程,比如要學習課程 0 你需要先學習課程 1 ,表示為[0,1] 給定n門課以及他們的先決條件,判斷是否可能完成所有課程?

樣例 給定 n = 2,先決條件為 [[1,0]] 返回 true 給定 n = 2,先決條件為 [[1,0],[0,1]] 返回 false

程式碼實現:

#採用拓撲排序方法
from queue import Queue

class Solution:
    # @param {int} numCourses a total of n courses
    # @param {int[][]} prerequisites a list of prerequisite pairs
    # @return {boolean} true if can finish all courses or false
    def canFinish(self, numCourses, prerequisites):
        # Write your code here
        edges = {i: [] for i in range(numCourses)}          #邊,建立一個空字典{0: [], 1: [], 2: []};初始化拓撲排序序列為空
        degrees = [0 for i in range(numCourses)]            #入度,出度的個數;[0, 0, 0, 0, 0, 0]
        for i, j in prerequisites:
            edges[j].append(i)
            degrees[i] += 1
        #import Queue
        queue, count = Queue(maxsize = numCourses), 0       # |  Create a queue object with a given maximum size.
        
        for i in range(numCourses):
            if degrees[i] == 0:         #入度為零的點放到寬度優先搜尋的佇列中
                queue.put(i)        # |      Put an item into the queue.

        while not queue.empty():        #直到佇列為空時候,演算法結束
            node = queue.get()          # 把佇列queue中的點釋放出來,放到拓撲中去。|      Remove and return an item from the queue.
            count += 1

            for x in edges[node]:   #每次釋放出點A的時候,就訪問A的相鄰點。
                degrees[x] -= 1     #並把這些點的入度減一
                if degrees[x] == 0: #如果發現某個點的入度減一變為0後,就把他放入佇列queue中
                    queue.put(x)

        return count == numCourses


my_solution = Solution()
data = [[1,0],[0,1]]
n = 2
order = my_solution.canFinish(n, data)  #列印二叉樹分層遍歷結果;levelOrder括號裡面是root,但是我這裡放入的是整個二叉樹,怎麼就沒有報錯
print(order)

lintcode實戰2:

127. 拓撲排序 給定一個有向圖,圖節點的拓撲排序定義如下:

對於圖中的每一條有向邊 A -> B , 在拓撲排序中A一定在B之前. 拓撲排序中的第一個節點可以是圖中的任何一個沒有其他節點指向它的節點. 針對給定的有向圖找到任意一種拓撲排序的順序.

樣例 例如以下的圖:

picture

拓撲排序可以為:

[0, 1, 2, 3, 4, 5] [0, 2, 3, 1, 5, 4] 挑戰 能否分別用BFS和DFS完成?

用BFS演算法實現:


"""
Definition for a Directed graph node
class DirectedGraphNode:
    def __init__(self, x):
        self.label = x
        self.neighbors = []
"""

class Solution:
    """
    @param graph: A list of Directed graph node
    @return: A list of integer
    """
    def topSort(self, graph):
        node_to_indegree = self.get_indegree(graph)

        # bfs
        order = []      #拓撲序列
        start_nodes = [n for n in graph if node_to_indegree[n] == 0] #找到入度為0的節點
        queue = collections.deque(start_nodes)      #將所有入度為 0 的點,也就是那些沒有任何依賴的點,放到寬度優先搜尋的佇列中
        while queue:
            node = queue.popleft()      #將佇列中的點一個一個的釋放出來
            order.append(node)      #,放到拓撲序列中
            for neighbor in node.neighbors:     #每次釋放出某個點 A 的時候,就訪問 A 的相鄰點(所有A指向的點),並把這些點的入度減去 1。
                node_to_indegree[neighbor] -= 1 #並把這些點的入度減去 1。
                if node_to_indegree[neighbor] == 0:  #如果發現某個點的入度被減去 1 之後變成了 0,則放入佇列中。
                    queue.append(neighbor)
                
        return order        #整個演算法的出口,返回一個序列,這個序列人看不到,只有機器能看懂
    
    def get_indegree(self, graph):  #返回一個字典,字典的key是graph裡的節點,key對應的值為節點的入度
        node_to_indegree = {x: 0 for x in graph}  #建立空字典,圖裡每個節點的值都是0,x 就是圖中的節點;統計所有點的入度,並初始化拓撲序列為空。

        for node in graph: #D指向E,E沒有指向D,所以,E是D的鄰居,D不是E的鄰居
            for neighbor in node.neighbors:         #node.neighbors:就是節點A的所有相鄰點。然後取每一個鄰居neighbor;A 的相鄰點:所有A指向的點,不包括指向A的點
                node_to_indegree[neighbor] += 1     #然後它的每個鄰居indegree加一;這裡的鄰居其實特指出度的鄰居,比如D指向E,E自動加一。並不是所有鄰居那個概念

                
        return node_to_indegree

用DFS演算法實現:(不推薦面試的時候用)


"""
Definition for a Directed graph node
class DirectedGraphNode:
    def __init__(self, x):
        self.label = x
        self.neighbors = []
"""

class Solution:
    """
    @param graph: A list of Directed graph node
    @return: A list of integer
    """
    def topSort(self, graph):
        indegree = {}
        for x in graph:
            indegree[x] = 0

        for i in graph:
            for j in i.neighbors:
                indegree[j] += 1

        ans = []
        for i in graph:
            if indegree[i] == 0:
                self.dfs(i, indegree, ans)
        return ans
    
    def dfs(self, i, indegree, ans):
        ans.append(i)
        indegree[i] -= 1
        for j in i.neighbors:
            indegree[j] -= 1
            if indegree[j] == 0:
                self.dfs(j, indegree, ans)

演算法視覺化:

BFS實現拓撲排序視覺化:

深度優先搜尋的拓撲排序

深度優先搜尋也可以做拓撲排序,不過因為不容易理解,也並不推薦作為拓撲排序的主流演算法。

DFS實現拓撲排序視覺化:

認識你是我們的緣分,同學,等等,記得關注我。

微信掃一掃 關注該公眾號