leetcode-拓撲排序

leetcode-拓撲排序

之前刷了不少leetcode的題目,現在開始春招了,特地把之前寫的代碼上傳了github:https://github.com/czy36mengfei/leetcode,歡迎大家下載、star。

知識拓展: 拓撲排序

拓撲排序並非一種排序算法,它能得到一個 AOV 網絡的拓撲序列,用於判斷有向無環圖中是否有環,即可以判斷一系列活動是否有循環依賴;

具體例子:去店裏喫飯的問題:顧客要求先喫飯再付錢,商家要求先收錢再做菜,這就是循環依賴,拓撲排序就可以幫助我們判斷是否形成環。

步驟:找無前驅的結點(即入度爲 0 的結點),一個一個地刪去(使用隊列或者棧),刪的時候,把鄰居結點的入度 -1。

“拓撲排序”用於對有先後順序的任務的輸出,如果先後順序形成一個環,那麼就表示這些任務頭尾依賴,就永遠不能完成。

例題 – LeetCode 第 207 題:課程表

現在你總共有 n 門課需要選,記爲 0n-1

在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]

給定課程總量以及它們的先決條件,判斷是否可能完成所有課程的學習?

示例 1:

輸入: 2, [[1,0]] 
輸出: true
解釋: 總共有 2 門課程。學習課程 1 之前,你需要完成課程 0。所以這是可能的。
123

示例 2:

輸入: 2, [[1,0],[0,1]]
輸出: false
解釋: 總共有 2 門課程。學習課程 1 之前,你需要先完成課程 0;並且學習課程 0 之前,你還應先完成課程 1。這是不可能的。
123

說明:

  1. 輸入的先決條件是由邊緣列表表示的圖形,而不是鄰接矩陣。詳情請參見圖的表示法
  2. 你可以假定輸入的先決條件中沒有重複的邊。

提示:

  1. 這個問題相當於查找一個循環是否存在於有向圖中。如果存在循環,則不存在拓撲排序,因此不可能選取所有課程進行學習。
  2. 通過 DFS 進行拓撲排序 - 一個關於Coursera的精彩視頻教程(21分鐘),介紹拓撲排序的基本概念。
  3. 拓撲排序也可以通過 BFS 完成。

方法一:拓撲排序(Kahn 算法)

拓撲排序實際上應用的是貪心算法,貪心算法簡而言之:每一步最優,全局就最優)。具體到拓撲排序,每一次都輸出入度爲 0 的結點,並移除它、修改它指向的結點的入度,依次得到的結點序列就是拓撲排序的結點序列。如果圖中還有結點沒有被移除,則說明“不能完成所有課程的學習”。

拓撲排序保證了每個活動(在這題中是“課程”)的所有前驅活動都排在該活動的前面,並且可以完成所有活動。拓撲排序的結果不唯一。拓撲排序還可以用於檢測一個有向圖是否有環。相關的概念還有 AOV 網,這裏就不展開了。

具體做如下:

1、在開始排序前,掃描對應的存儲空間(使用鄰接表),將入度爲 00 的結點放入隊列。

2、只要隊列非空,就從隊首取出入度爲 0 的結點,將這個結點輸出到結果集中,並且將這個結點的所有鄰接結點(它指向的結點)的入度減 1,在減 1 以後,如果這個被減 1 的結點的入度爲 0 ,就繼續入隊。

3、當隊列爲空的時候,檢查結果集中的頂點個數是否和課程數相等即可。

class Solution(object):

    # 思想:該方法的每一步總是輸出當前無前趨(即入度爲零)的頂點

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        """
        :type numCourses: int 課程門數
        :type prerequisites: List[List[int]] 課程與課程之間的關係
        :rtype: bool
        """
        # 課程的長度
        clen = len(prerequisites)
        if clen == 0:
            # 沒有課程,當然可以完成課程的學習
            return True

        # 步驟1:統計每個頂點的入度
        # 入度數組,記錄了指向它的結點的個數,一開始全部爲 0
        in_degrees = [0 for _ in range(numCourses)]
        # 鄰接表,使用散列表是爲了去重
        adj = [set() for _ in range(numCourses)]

        # 想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]
        # [0, 1] 表示 1 在先,0 在後
        # 注意:鄰接表存放的是後繼 successor 結點的集合
        for second, first in prerequisites:
            in_degrees[second] += 1
            adj[first].add(second)

        # 步驟2:拓撲排序開始之前,先把所有入度爲 0 的結點加入到一個隊列中
        # 首先遍歷一遍,把所有入度爲 0 的結點都加入隊列
        queue = []
        for i in range(numCourses):
            if in_degrees[i] == 0:
                queue.append(i)

        counter = 0
        while queue:
            top = queue.pop(0)
            counter += 1
            # 步驟3:把這個結點的所有後繼結點的入度減去 1,如果發現入度爲 0 ,就馬上添加到隊列中
            for successor in adj[top]:
                in_degrees[successor] -= 1
                if in_degrees[successor] == 0:
                    queue.append(successor)

        return counter == numCourses

方法二:深度優先遍歷

這裏要使用逆鄰接表。其實就是檢測這個有向圖中有沒有環,只要存在環,這些課程就不能按要求學完。

具體方法是:

第 1 步:構建逆鄰接表;

第 2 步:遞歸處理每一個還沒有被訪問的結點,具體做法很簡單:對於一個結點來說,先輸出指向它的所有頂點,再輸出自己

第 3 步:如果這個頂點還沒有被遍歷過,就遞歸遍歷它,把所有指向它的結點都輸出了,再輸出自己。注意:當訪問一個結點的時候,應當先遞歸訪問它的前驅結點,直至前驅結點沒有前驅結點爲止

class Solution(object):

    # 這裏使用逆鄰接表

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        """
        :type numCourses: int 課程門數
        :type prerequisites: List[List[int]] 課程與課程之間的關係
        :rtype: bool
        """
        # 課程的長度
        clen = len(prerequisites)
        if clen == 0:
            # 沒有課程,當然可以完成課程的學習
            return True
        # 深度優先遍歷,判斷結點是否訪問過
        # 這裏要設置 3 個狀態
        # 0 就對應 False ,表示結點沒有訪問過
        # 1 就對應 True ,表示結點已經訪問過,在深度優先遍歷結束以後才置爲 1
        # 2 表示當前正在遍歷的結點,如果在深度優先遍歷的過程中,
        # 有遇到狀態爲 2 的結點,就表示這個圖中存在環
        visited = [0 for _ in range(numCourses)]

        # 逆鄰接表,存的是每個結點的前驅結點的集合
        # 想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]
        # 1 在前,0 在後
        inverse_adj = [set() for _ in range(numCourses)]
        for second, first in prerequisites:
            inverse_adj[second].add(first)

        for i in range(numCourses):
            # 在遍歷的過程中,如果發現有環,就退出
            if self.__dfs(i, inverse_adj, visited):
                return False
        return True

    def __dfs(self, vertex, inverse_adj, visited):
        """
        注意:這個遞歸方法的返回值是返回是否有環
        :param vertex: 結點的索引
        :param inverse_adj: 逆鄰接表,記錄的是當前結點的前驅結點的集合
        :param visited: 記錄了結點是否被訪問過,2 表示當前正在 DFS 這個結點
        :return: 是否有環,返回 True 表示這個有向圖有環
        """
        # 2 表示這個結點正在訪問
        if visited[vertex] == 2:
            # 表示遇到環
            return True
        if visited[vertex] == 1:
            return False

        visited[vertex] = 2
        for precursor in inverse_adj[vertex]:
            # 如果有環,就返回 True 表示有環
            if self.__dfs(precursor, inverse_adj, visited):
                return True

        # 1 表示訪問結束
        # 先把 vertex 這個結點的所有前驅結點都輸出之後,再輸出自己
        visited[vertex] = 1
        return False

點我有驚喜

每日一題

課程表 II

現在你總共有 n 門課需要選,記爲 0 到 n-1。

在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]

給定課程總量以及它們的先決條件,返回你爲了學完所有課程所安排的學習順序。

可能會有多個正確的順序,你只要返回一種就可以了。如果不可能完成所有課程,返回一個空數組。

示例 1:

輸入: 2, [[1,0]]
輸出: [0,1]
解釋: 總共有 2 門課程。要學習課程 1,你需要先完成課程 0。因此,正確的課程順序爲 [0,1] 。
示例 2:

輸入: 4, [[1,0],[2,0],[3,1],[3,2]]
輸出: [0,1,2,3] or [0,2,1,3]
解釋: 總共有 4 門課程。要學習課程 3,你應該先完成課程 1 和課程 2。並且課程 1 和課程 2 都應該排在課程 0 之後。
因此,一個正確的課程順序是 [0,1,2,3] 。另一個正確的排序是 [0,2,1,3] 。
說明:

輸入的先決條件是由邊緣列表表示的圖形,而不是鄰接矩陣。詳情請參見圖的表示法。
你可以假定輸入的先決條件中沒有重複的邊。
提示:

這個問題相當於查找一個循環是否存在於有向圖中。如果存在循環,則不存在拓撲排序,因此不可能選取所有課程進行學習。
通過 DFS 進行拓撲排序 - 一個關於Coursera的精彩視頻教程(21分鐘),介紹拓撲排序的基本概念。
拓撲排序也可以通過 BFS 完成。

來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/course-schedule-ii

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