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

更新於 2024/12/20閱讀時間約 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


avatar-img
90會員
425內容數
由有業界實戰經驗的演算法工程師, 手把手教你建立解題的框架, 一步步寫出高效、清晰易懂的解題答案。 著重在讓讀者啟發思考、理解演算法,熟悉常見的演算法模板。 深入淺出地介紹題目背後所使用的演算法意義,融會貫通演算法與資料結構的應用。 在幾個經典的題目融入一道題目的多種解法,或者同一招解不同的題目,擴展廣度,並加深印象。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
題目敘述 題目會給定我們一顆二元搜索樹的根結點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
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
題目敘述 題目會給定一棵二元樹的根結點, 要求我們計算滿足局部路徑節點和=targetSum的數目有多少? 註: 局部路徑節點和 =由節點a往下走到某個節點b,這個區間內的節點值總和 題目的原文敘述 測試範例 Example 1: Input: root = [10,5,-3,3
Thumbnail
題目敘述 題目會給定一個二維陣列grid,代表每顆橘子分布的位置和初始狀態。 0: 這個格子點沒有東西。 1: 這個格子點有一顆新鮮的橘子。 2: 這個格子點有一顆壞掉的橘子。 壞掉的橘子上面的黴菌, 每隔一個週期,可以向上、下、左、右 N4四連通的格子點感染一次。 請問,最少需要多
Thumbnail
題目會給定我們一個輸入陣列connections,和城市的總數目n。 輸入陣列裡面是以pair的方式儲存,(a, b) 分邊代表這條邊的起點和終點。 請問,我們需要改變多少條邊的方向,才能讓每條路徑都指向編號零號的城市( City #0)? 註: 題目還保證,在改變方向之後,一定可以讓每座城
Thumbnail
題目敘述 題目會給我們一串相鄰矩陣isConnected,相鄰矩陣的元素值isConnected[i][j] 代表第i座城市和第j座城市是否有連通。 如果彼此有連通,則isConnected[i][j]=1。 如果彼此沒有連通,則isConnected[i][j]=0。 彼此互相有路徑可以
Thumbnail
題目敘述 題目會給我們一個輸入陣列prerequisites,每個pair代表兩個課程之間的先修關係,和課程總數numCourses。 題目問我們這組課程表是否能依照順序修完所有的課程? 如果可以,返回True。 如果不行,代表有擋修形成死結,無法依照順序修完所有的課程,返回False。
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Thumbnail
題目敘述 題目會給定一棵二元樹的根結點, 要求我們計算滿足局部路徑節點和=targetSum的數目有多少? 註: 局部路徑節點和 =由節點a往下走到某個節點b,這個區間內的節點值總和 題目的原文敘述 測試範例 Example 1: Input: root = [10,5,-3,3
Thumbnail
題目敘述 題目會給定一個二維陣列grid,代表每顆橘子分布的位置和初始狀態。 0: 這個格子點沒有東西。 1: 這個格子點有一顆新鮮的橘子。 2: 這個格子點有一顆壞掉的橘子。 壞掉的橘子上面的黴菌, 每隔一個週期,可以向上、下、左、右 N4四連通的格子點感染一次。 請問,最少需要多
Thumbnail
題目會給定我們一個輸入陣列connections,和城市的總數目n。 輸入陣列裡面是以pair的方式儲存,(a, b) 分邊代表這條邊的起點和終點。 請問,我們需要改變多少條邊的方向,才能讓每條路徑都指向編號零號的城市( City #0)? 註: 題目還保證,在改變方向之後,一定可以讓每座城
Thumbnail
題目敘述 題目會給我們一串相鄰矩陣isConnected,相鄰矩陣的元素值isConnected[i][j] 代表第i座城市和第j座城市是否有連通。 如果彼此有連通,則isConnected[i][j]=1。 如果彼此沒有連通,則isConnected[i][j]=0。 彼此互相有路徑可以
Thumbnail
題目敘述 題目會給我們一個輸入陣列prerequisites,每個pair代表兩個課程之間的先修關係,和課程總數numCourses。 題目問我們這組課程表是否能依照順序修完所有的課程? 如果可以,返回True。 如果不行,代表有擋修形成死結,無法依照順序修完所有的課程,返回False。