圖論應用經典 課程表 Course Schedule_Leetcode #207

閱讀時間約 12 分鐘

題目敘述

題目會給我們一個輸入陣列prerequisites,每個pair代表兩個課程之間的先修關係,和課程總數numCourses。

題目問我們這組課程表是否能依照順序修完所有的課程?

如果可以,返回True。

如果不行,代表有擋修形成死結,無法依照順序修完所有的課程,返回False。


詳細的題目可在這裡看到


測試範例

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0. So it is possible.

Example 2:

Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation: There are a total of 2 courses to take.
To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.

0, 1兩個課程彼此擋修,形成互卡的死結,因此無法依照順序修完所有的課程。​

約束條件

  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • All the pairs prerequisites[i] are unique.

演算法 DFS

本文章將介紹兩種方法。

  1. 可以用DFS的cycle detection環路偵測演算法來偵測是否有擋修互卡的死結。
  2. 可以用BFS的Topological sort拓樸排序,來檢查是存在擋修互卡的死結。


  1. 可以用DFS的cycle detection環路偵測演算法
    每個節點對應到課程,每個先修關係對應到一條邊。
    造一個loop,從編號小的節點開始進行DFS深度優先拜訪。
    假如遇到當下這個節點是Checking狀態,代表存在死結,無法依照順序修完所有的課程。
    假如當下這個節點尚未檢查過,把自己標記成Checking,接著遞迴呼叫DFS拜訪所有鄰居節點。
    最後,假如都順利完成,把當下這個節點(也就是這門課程)標記為Completed。


image

image

image

image


image

image

image

image


程式碼 DFS的cycle detection環路偵測演算法

class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:

# Constant defined for course state
NOT_CHECKED, CHECKING, COMPLETED = 0, 1, 2

# -------------------------------

def has_deadlock( course )->bool:

if course_state[course] != NOT_CHECKED:
# There is a cycle(i.e., deadlock ) in prerequisites
return course_state[course] == CHECKING

# update current course as checking
course_state[course] = CHECKING

# check pre_course in DFS and detect whether there is deadlock
for pre_course in requirement[course]:

if has_deadlock( pre_course ):
# deadlock is found, impossible to finish all courses
return True


# update current course as completed
course_state[course] = COMPLETED

return False

# -------------------------------

# each course maintain a list of its own prerequisites
requirement = collections.defaultdict( list )

for course, pre_course in prerequisites:
requirement[course].append( pre_course )


# each course maintain a state among {NOT_CHECKED, CHECKING, COMPLETED}
# Initial state is NOT_CHECKED
course_state = [ NOT_CHECKED for _ in range(numCourses) ]

# Launch cycle (i.e., deadlock ) detection in DFS
for course_idx in range(0, numCourses):

if has_deadlock(course_idx):
# deadlock is found, impossible to finish all courses
return False

# we can finish all course with required order
return True

演算法 BFS

另一種就是Topological sort拓樸排序。

建造一個Queue,從沒有擋修的課程(in_degree = 0的節點)開始進行廣度優先搜索,移除這一輪沒有擋修的課程和對應的out-going edge,移除的同時,把下一輪沒有擋修的課程加入Queue中,反覆進行迭代。

最後,如果所有課程都移除完畢,而且沒有剩餘的課程,代表可以依照順序修完所有課程。否則,代表存在有擋修互卡的死結。


程式碼 BFS的Topological sort拓樸排序

class Solution:
 def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
  
  # each course maintain a list of its own dependency
  relation = defaultdict( list )

  # in degree of each course
  # in degree stands for the number of preliminary courses
  in_degree = defaultdict( int )
  
  for course, pre_course in prerequisites:
   # update relation
   relation[pre_course].append( course )

   # update in_degree
   in_degree[course] += 1


  # Topological sort on in_degree
  bfs_q = deque()
  for course_idx in range(0, numCourses):
   if in_degree[course_idx] == 0:
    bfs_q.append(course_idx)

  finish_count = 0
  while bfs_q:

   # Finish current course
   cur_course = bfs_q.popleft()
   finish_count += 1

   # Unlock some courses related to current course
   for course in relation[cur_course]:
    in_degree[course] -= 1

    if in_degree[course] == 0:
     # this course can be taken next round if it doesn't have preliminary course anymore
     bfs_q.append( course )
  
  return finish_count == numCourses ​

複雜度分析

時間複雜度:

O( V+E )

兩種方法皆為O(V+E),點至多拜訪一次,邊也是至多拜訪一次。

空間複雜度:

O( V+E )

兩種方法皆為O(V+E),包含對應的DFS遞迴深度 + table大小,或者BFS Queue長度 + table大小


關鍵知識點

  1. 把課程先修關係抽象化,轉為圖論中的節點(課程)和邊(先修關係),把課程擋修的問題轉化為是否存在Cycle環路的問題


  1. 圖論中的題目,假如沒有特殊限制或者還沒有具體的解題想法,先聯想到DFS和BFS,這兩個可以說是圖論中最泛用的演算法模板

Reference:

[1] Python by DFS//BFS and cycle detection [w/ Graph ] - Course Schedule - LeetCode


82會員
417Content count
由有業界實戰經驗的演算法工程師, 手把手教你建立解題的框架, 一步步寫出高效、清晰易懂的解題答案。 著重在讓讀者啟發思考、理解演算法,熟悉常見的演算法模板。 深入淺出地介紹題目背後所使用的演算法意義,融會貫通演算法與資料結構的應用。 在幾個經典的題目融入一道題目的多種解法,或者同一招解不同的題目,擴展廣度,並加深印象。
留言0
查看全部
發表第一個留言支持創作者!
題目敘述 題目會給定我們一顆二元搜索樹的根結點root,和任意兩個樹中的節點p和q。 要求我們找出p, q最靠近的公共祖先節點。 題目的原文敘述 測試範例 Example 1: Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q
題目會給我們一個pair陣列,裡面都是原本陣列相鄰元素形成的pair,順序已經被打散。 要求我們從pair陣列重建出原本的陣列。 答案可能有不只一組,任選一組回傳即可。
題目會給一顆二元樹,要求我們計算節點值 和 子樹平均值相等的node有幾個。
題目會給我們節點總數目n、左子樹的關係陣列、右子樹的關係陣列,要求我們驗證在給定的條件下,能不能構成一顆合法的二元樹。
題目會給定兩個參數,一個是底數x,一個是次方n。要求我們計算出x^n。 要求實作myPow(self, x: float, n: int) -> float 函數的內部邏輯。 也就是說,不可以呼叫程式語言內建計算指數的library
題目的輸入會給我們一個串列,要求我們從頭到尾反轉整個串列。 例子: 如果輸入是1 -> 2 -> 3,那麼輸出就是 3 -> 2 -> 1
題目敘述 題目會給定我們一顆二元搜索樹的根結點root,和任意兩個樹中的節點p和q。 要求我們找出p, q最靠近的公共祖先節點。 題目的原文敘述 測試範例 Example 1: Input: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q
題目會給我們一個pair陣列,裡面都是原本陣列相鄰元素形成的pair,順序已經被打散。 要求我們從pair陣列重建出原本的陣列。 答案可能有不只一組,任選一組回傳即可。
題目會給一顆二元樹,要求我們計算節點值 和 子樹平均值相等的node有幾個。
題目會給我們節點總數目n、左子樹的關係陣列、右子樹的關係陣列,要求我們驗證在給定的條件下,能不能構成一顆合法的二元樹。
題目會給定兩個參數,一個是底數x,一個是次方n。要求我們計算出x^n。 要求實作myPow(self, x: float, n: int) -> float 函數的內部邏輯。 也就是說,不可以呼叫程式語言內建計算指數的library
題目的輸入會給我們一個串列,要求我們從頭到尾反轉整個串列。 例子: 如果輸入是1 -> 2 -> 3,那麼輸出就是 3 -> 2 -> 1
你可能也想看
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!