LeetCode 207. 課程表 | Python
207. 課程表
題目來源:力扣(LeetCode)https://leetcode-cn.com/problems/course-schedule
題目
你這個學期必須選修 numCourse 門課程,記為 0 到 numCourse-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 <= numCourses <= 10^5
解題思路
思路:拓撲排序(BFS,DFS)
其實,這是一道經典的【拓撲排序】問題。
首先先審題,結合示例 1 和示例 2,我們其實可以看到,其實題目問的是給定輸入先決條件表示的圖形(也就是課程表)是否是有向無環圖。也是說,當確定先決條件之後,圖形不能存在環,否則不成立。
有向無環圖(DAG):指的一個有向圖從任意頂點出發無法經過若干條邊回到該點,則該圖是一個有向無環圖。
有向無環圖,詳細資訊可由下方入口進行了解
https://en.wikipedia.org/wiki/Directed_acyclic_graph (維基百科:有向無環圖)
https://baike.baidu.com/item/%E6%9C%89%E5%90%91%E6%97%A0%E7%8E%AF%E5%9B%BE/10972513?fr=aladdin (百度百科:有向無環圖)
那麼當要求課程安排圖是否是有向無環圖時,我們用拓撲排序來判斷。拓撲排序:將圖所有頂點進行排序成線性序列,使得圖中任意一個有向邊 uv,u 線上性序列中出現在 v 前面。
拓撲排序,詳細資訊可由下方入口進行了解
https://en.wikipedia.org/wiki/Topological_sorting(維基百科:拓撲排序)
https://baike.baidu.com/item/%E6%8B%93%E6%89%91%E6%8E%92%E5%BA%8F/5223807?fr=aladdin (百度百科:拓撲排序)
在題目中的提示中有說明,輸入的先決條件時由 邊緣列表 表示,在這裡,我們要將此轉換為 鄰接表。
鄰接表,詳細資訊可由下方入口進行了解
https://en.wikipedia.org/wiki/Adjacency_list (維基百科:鄰接表)
https://baike.baidu.com/item/%E9%82%BB%E6%8E%A5%E8%A1%A8/9796152?fr=aladdin (百度百科:鄰接表)
廣度優先搜尋(BFS)
首先,先將邊緣列表表示的先決條件轉換得到鄰接表。然後統計圖中每個節點的入度情況,儲存在一個列表中。(當入度為 0 時,表示點不作為任何邊的終點,也就說該點是所有連線邊的起點)
在這裡,定義輔助佇列 queue
,將所有入度為 0 的節點存入佇列中,用於後續處理。
開始進行搜尋,令佇列中的節點出隊:
- 將當前節點鄰接節點入度減 1;
- 當鄰接節點入度為 0 時,將其入隊。
當佇列中的節點出隊時,令課程總量減 1:
- 如果課程圖是有向無環圖,若完成拓撲排序,那麼所有的節點都會入隊出隊;
- 若是課程圖存在環,那麼一定存在節點入度不為 0 的情況;
- 也就是說,當所有節點入隊出隊後,課程總量為 0 的情況下,也就能證明課程圖是否是有向無環圖。
具體的程式碼見【程式碼實現 # 廣度優先搜尋】
深度優先搜尋(DFS)
我們先看使用深度優先搜尋的思路來判斷圖是否有環。
在這裡,我們藉助一個輔助列表(當然也可以考慮用棧)標記每個節點的狀態:
- 未標記,令此時狀態為 0;
- 臨時標記,令此時狀態為 1;
- 永久標記,令此時狀態為 -1。
開始對每個節點進行深度優先搜尋,當存在環時,返回 False,執行過程如下:
- 當節點狀態為 -1 時,表示已永久標記,此時已搜尋完畢,不許重複搜尋,直接返回 True;
- 當節點狀態為 1 時,表示臨時標記,也就說此節點,在此次深搜中又一次對該節點進行了搜尋,那麼表示圖中存在環,那麼直接返回 False;
- 當狀態為 0 時,先將節點臨時標記(令狀態為 1),然後以此點繼續搜尋,直至遇到終止條件。
- 當節點搜尋完畢,未發現閉環,則去除臨時標記,將節點進行永久標記(令狀態為 -1)
如果所有節點都搜尋完畢後,不存在環,則返回 True。
具體的程式碼見【程式碼實現 # 深度優先搜尋】
程式碼實現
# 廣度優先搜尋
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
from collections import deque
# 將邊緣列表表示的先決條件轉化為 鄰接表
adjacency = [[] for _ in range(numCourses)]
# 定義列表統計圖中每個節點的入度情況
indegree = [0] * numCourses
for info in prerequisites:
adjacency[info[1]].append(info[0])
indegree[info[0]] += 1
queue = deque()
# 將入度為 0 的節點入隊
for i in range(len(indegree)):
if not indegree[i]:
queue.append(i)
# 開始進行搜尋
while queue:
u = queue.popleft()
numCourses -= 1
# 搜尋鄰接節點
for v in adjacency[u]:
# 將鄰接節點入度減 1
indegree[v] -= 1
# 如果入度為 0,入隊
if indegree[v] == 0:
queue.append(v)
return numCourses == 0
# 深度優先搜尋
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
def dfs(adjacency, sign, i):
if sign[i] == -1:
return True
if sign[i] == 1:
return False
# 開始搜尋,先進行臨時標記
sign[i] = 1
# 以此節點往下搜尋
for j in adjacency[i]:
if not dfs(adjacency, sign, j):
return False
sign[i] = -1
return True
# 定義輔助列表標記狀態,初始化為 0,表示未標記
sign = [0] * numCourses
# 將邊緣列表表示的先決條件轉化為 鄰接表
adjacency = [[] for _ in range(numCourses)]
for info in prerequisites:
adjacency[info[1]].append(info[0])
# 開始深搜
for i in range(numCourses):
if not dfs(adjacency, sign, i):
return False
return True
實現結果
歡迎關注
公眾號 【書所集錄】