LeetCode 專題:拓撲排序

知識點總結

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

解決一個工程中的任務是否能夠順利完成,判斷是否出現環。(補充:還有一種判斷圖中是否有環的數據結構是“并查集”)。

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

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

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

因此“拓撲排序”還可以用于檢測一個圖中是否有環。

LeetCode 上拓撲排序目前(截止 2019 年 2 月 16 日早)一共有 5 題。

LeetCode 專題:拓撲排序-1

我們就做兩題。

LeetCode 第 207 題:課程表

傳送門:207. 課程表。

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

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

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

示例 1:

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

示例 2:

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

說明:

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

提示:

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

思路1:Kahn 算法,即拓撲排序。構建的鄰接表就是我們通常認識的鄰接表,每一個結點存放的是后繼結點的集合。

該方法的每一步總是輸出當前無前趨(即入度為零)的頂點。為避免每次選入度為 0 的頂點時掃描整個存儲空間,可設置一個隊列暫存所有入度為 0 的頂點。

具體做法如下:

1、在開始排序前,掃描對應的存儲空間,將入度為 0 的頂點均入隊列。

2、只要隊列非空,就從隊首取出入度為 0 的頂點,將這個頂點輸出到結果集中,并且將這個頂點的所有鄰接點的入度減 1,在減 1 以后,發現這個鄰接點的入度為 0 ,就繼續入隊。

最后檢查結果集中的頂點個數是否和課程數相同即可。

Python 代碼:

class Solution(object):

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

    def canFinish(self, numCourses, prerequisites):
        """
        :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)

        # print("in_degrees", in_degrees)
        # 首先遍歷一遍,把所有入度為 0 的結點加入隊列
        res = []
        queue = []
        # 步驟2:拓撲排序開始之前,先把所有入度為 0 的結點加入到一個隊列中
        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

思路2:構建逆鄰接表,實現深度優先遍歷。思路其實也很簡單,其實就是檢測這個有向圖中有沒有環,只要存在環,課程就不能完成。

第 1 步:構造逆鄰接表;
第 2 步:遞歸處理每一個還沒有被訪問的結點;核心思想是:對于一個頂點 vertex 來說,我們先輸出了指向它的所有頂點,然后再輸出自己,就是這么簡單。
第 3 步:如果這個頂點沒有被遍歷過,就遞歸遍歷它,把所有指向它的結點都輸出了,再輸出自己。

注意:這個深度優先遍歷得通過逆鄰接表實現,當訪問一個結點的時候,應該遞歸訪問它的前驅結點,直至前驅結點沒有前驅結點為止。

Python 代碼:

# 207. 課程表
# 現在你總共有 n 門課需要選,記為 0 到 n-1。
# 在選修某些課程之前需要一些先修課程。 例如,想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]
# 給定課程總量以及它們的先決條件,判斷是否可能完成所有課程的學習?
class Solution(object):

    # 這里使用逆鄰接表

    def canFinish(self, numCourses, prerequisites):
        """
        :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: 是否有環
        """
        # 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

這兩種思路都可以完成 LeetCode 第 210 題。

LeetCode 第 210 題:課程表 II

傳送門:210. 課程表 II

現在你總共有 n 門課需要選,記為 0n-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] 。

說明:

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

提示:

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

思路1:拓撲排序。

Python 代碼:

class Solution(object):
    def findOrder(self, numCourses, prerequisites):
        """
        :type numCourses: int 課程門數
        :type prerequisites: List[List[int]] 課程與課程之間的關系
        :rtype: bool
        """
        # 課程的長度
        clen = len(prerequisites)
        if clen == 0:
            # 沒有課程,當然可以完成課程的學習
            return [i for i in range(numCourses)]
        # 入度數組,一開始全部為 0
        in_degrees = [0 for _ in range(numCourses)]
        # 鄰接表
        adj = [set() for _ in range(numCourses)]
        # 想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]
        # 1 -> 0,這里要注意:不要弄反了
        for second, first in prerequisites:
            in_degrees[second] += 1
            adj[first].add(second)

        # print("in_degrees", in_degrees)
        # 首先遍歷一遍,把所有入度為 0 的結點加入隊列
        res = []
        queue = []
        for i in range(numCourses):
            if in_degrees[i] == 0:
                queue.append(i)

        while queue:
            top = queue.pop(0)
            res.append(top)

            for successor in adj[top]:
                in_degrees[successor] -= 1
                if in_degrees[successor] == 0:
                    queue.append(successor)
        if len(res) != numCourses:
            return []
        return res

思路2:基于逆鄰接表的深度優先遍歷。

Python 代碼:

class Solution(object):

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

        # 逆鄰接表
        inverse_adj = [set() for _ in range(numCourses)]
        # 想要學習課程 0 ,你需要先完成課程 1 ,我們用一個匹配來表示他們: [0,1]
        # 1 -> 0,這里要注意:不要弄反了
        for second, first in prerequisites:
            inverse_adj[second].add(first)

        visited = [0 for _ in range(numCourses)]
        # print("in_degrees", in_degrees)
        # 首先遍歷一遍,把所有入度為 0 的結點加入隊列

        res = []
        for i in range(numCourses):
            if self.__dfs(i,inverse_adj, visited, res):
                return []
        return res

    def __dfs(self, vertex, inverse_adj, visited, res):
        """
        注意:這個遞歸方法的返回值是返回是否有環
        :param vertex: 結點的索引
        :param inverse_adj: 逆鄰接表,記錄的是當前結點的前驅結點的集合
        :param visited: 記錄了結點是否被訪問過,2 表示當前正在 DFS 這個結點
        :return: 是否有環
        """
        # 2 表示這個結點正在訪問
        if visited[vertex] == 2:
            # DFS 的時候如果遇到一樣的結點,就表示圖中有環,課程任務便不能完成
            return True
        if visited[vertex] == 1:
            return False
        # 表示正在訪問這個結點
        visited[vertex] = 2
        # 遞歸訪問前驅結點
        for precursor in inverse_adj[vertex]:
            # 如果沒有環,就返回 False,
            # 執行以后,逆拓撲序列就存在 res 中
            if self.__dfs(precursor, inverse_adj, visited, res):
                return True

        # 能走到這里,說明所有的前驅結點都訪問完了,所以可以輸出了
        # 并且將這個結點狀態置為 1
        visited[vertex] = 1

        # 先把 vertex 這個結點的所有前驅結點都輸出之后,再輸出自己
        res.append(vertex)
        # 最后不要忘記返回 False 表示無環
        return False

(本節完)


二、關鍵路徑

重要的概念:(1)關鍵路徑(理解為什么是權值最大的)(2)關鍵活動

參考資料:

1、《大話數據結構》

2、極客時間《算法與數據結構之美》,作者:王爭

3、自己以前寫的題解:

LeetCode 題解之 207. Course Schedule(拓撲排序模板題 1 )

https://blog.csdn.net/lw_power/article/details/80795288

LeetCode 題解之 210. Course Schedule II(拓撲排序模板題 2 )

https://blog.csdn.net/lw_power/article/details/80795355

4、深入理解拓撲排序(Topological sort)

http://www.lxweimin.com/p/3347f54a3187

拓撲排序和 dfs

解決一個有依賴的工程是否能夠完成。

拓撲排序和關鍵路徑問題。

1、AOV 網:與頂點相關。圖中不允許有回路。

2、AOE 網:與邊相關。整個任務是否能夠完成取決于最長的關鍵路徑。

使用拓撲排序判斷 DAG 是否有回路。

LeetCode 第 207 題:課程表

參考資料:https://blog.csdn.net/ljiabin/article/details/45846837

拓撲排序的原理:在一個有向圖中,每次找到一個沒有前驅節點的結點(也就是入度為 0 的結點),然后把它指向的結點的邊都去掉,==重復這個過程(BFS)==,直到所有結點已被找到,或者沒有符合條件的節點(如果圖中有環存在)。

回顧一下圖的三種表示方式:

1、邊表示法(即題目中表示方法);

2、鄰接表法;

3、鄰接矩陣表示法。

用鄰接表存儲圖比較方便尋找入度為 0 的節點。

拓撲排序以及拓撲排序的偽代碼:

LeetCode 專題:拓撲排序-2
LeetCode 專題:拓撲排序-3

拓撲排序的寫法:

https://blog.csdn.net/qq508618087/article/details/50748965

LeetCode 專題:拓撲排序-4

LeetCode 第 210 題:課程表 II

要求返回一種拓撲排序的結果。

參考資料:http://zxi.mytechroad.com/blog/graph/leetcode-210-course-schedule-ii/

LeetCode 專題:拓撲排序-5

dfs 的做法:Java 的寫法以被指向的課程作為鍵。

LeetCode 專題:拓撲排序-6

注意:下面這個 dfs 的方法,有向圖的表示以先行課程作為鍵。

LeetCode 專題:拓撲排序-7
LeetCode 專題:拓撲排序-8

參考資料:拓撲排序:https://mp.weixin.qq.com/s/rRIz_rsp6I9zX-EiOjCCaQ

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容