leetcode-拓撲排序
之前刷了不少leetcode的題目,現在開始春招了,特地把之前寫的代碼上傳了github:https://github.com/czy36mengfei/leetcode,歡迎大家下載、star。
知識拓展: 拓撲排序
拓撲排序並非一種排序算法,它能得到一個 AOV 網絡的拓撲序列,用於判斷有向無環圖中是否有環,即可以判斷一系列活動是否有循環依賴;
具體例子:去店裏喫飯的問題:顧客要求先喫飯再付錢,商家要求先收錢再做菜,這就是循環依賴,拓撲排序就可以幫助我們判斷是否形成環。
步驟:找無前驅的結點(即入度爲 0 的結點),一個一個地刪去(使用隊列或者棧),刪的時候,把鄰居結點的入度 -1。
“拓撲排序”用於對有先後順序的任務的輸出,如果先後順序形成一個環,那麼就表示這些任務頭尾依賴,就永遠不能完成。
例題 – LeetCode 第 207 題:課程表
現在你總共有 n 門課需要選,記爲
0
到n-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
說明:
- 輸入的先決條件是由邊緣列表表示的圖形,而不是鄰接矩陣。詳情請參見圖的表示法。
- 你可以假定輸入的先決條件中沒有重複的邊。
提示:
- 這個問題相當於查找一個循環是否存在於有向圖中。如果存在循環,則不存在拓撲排序,因此不可能選取所有課程進行學習。
- 通過 DFS 進行拓撲排序 - 一個關於Coursera的精彩視頻教程(21分鐘),介紹拓撲排序的基本概念。
- 拓撲排序也可以通過 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