LeetCode 題解之 210. Course Schedule II(拓撲排序模板題 2 )
阿新 • • 發佈:2019-01-28
210. Course Schedule 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 完成。
思路分析
求解關鍵:這道題可以說是一道拓撲排序的模板題,也可以使用深度優先遍歷完成。
我個人覺得使用拓撲排序思路會更清晰一些,dfs 遞迴要判斷結點的狀態,有那麼一些繞。
參考解答
參考解答1:使用拓撲排序。
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
public class Solution {
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 先處理極端情況
if (numCourses <= 0) {
return new int[0];
}
// 鄰接表表示
HashSet<Integer>[] graph = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new HashSet<>();
}
// 入度表
int[] inDegree = new int[numCourses];
// 遍歷 prerequisites 的時候,把 鄰接表 和 入度表 都填上
for (int[] p : prerequisites) {
graph[p[1]].add(p[0]);
inDegree[p[0]]++;
}
LinkedList<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegree[i] == 0) {
queue.addLast(i);
}
}
ArrayList<Integer> res = new ArrayList<>();
while (!queue.isEmpty()) {
// 當前入度為 0 的結點
Integer inDegreeNode = queue.removeFirst();
// 加入結果集中
res.add(inDegreeNode);
// 下面從圖中刪去
// 得到所有的後繼課程,接下來把它們的入度全部減去 1
HashSet<Integer> nextCourses = graph[inDegreeNode];
for (Integer nextCourse : nextCourses) {
inDegree[nextCourse]--;
// 馬上檢測該結點的入度是否為 0,如果為 0,馬上加入佇列
if (inDegree[nextCourse] == 0) {
queue.addLast(nextCourse);
}
}
}
// 如果結果集中的數量不等於結點的數量,就不能完成課程任務,這一點是拓撲排序的結論
int resLen = res.size();
if (resLen == numCourses) {
int[] ret = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
ret[i] = res.get(i);
}
return ret;
} else {
return new int[0];
}
}
}
參考解答2:使用深度優先遍歷。
import java.util.HashSet;
import java.util.Stack;
/**
* @author liwei
* @date 18/6/24 下午4:10
*/
public class Solution3 {
/**
* @param numCourses
* @param prerequisites
* @return
*/
public int[] findOrder(int numCourses, int[][] prerequisites) {
if (numCourses <= 0) {
// 連課程數目都沒有,就根本沒有辦法完成練習了,根據題意應該返回空陣列
return new int[0];
}
int plen = prerequisites.length;
if (plen == 0) {
// 沒有有向邊,則表示不存在課程依賴,任務一定可以完成
int[] ret = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
ret[i] = i;
}
return ret;
}
int[] marked = new int[numCourses];
// 初始化有向圖 begin
HashSet<Integer>[] graph = new HashSet[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new HashSet<>();
}
// 初始化有向圖 end
// 有向圖的 key 是前驅結點,value 是後繼結點的集合
for (int[] p : prerequisites) {
graph[p[1]].add(p[0]);
}
// 使用 Stack 或者 List 記錄遞迴的順序,這裡使用 Stack
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < numCourses; i++) {
if (dfs(i, graph, marked, stack)) {
// 注意方法的語義,如果圖中存在環,表示課程任務不能完成,應該返回空陣列
return new int[0];
}
}
// 在遍歷的過程中,一直 dfs 都沒有遇到已經重複訪問的結點,就表示有向圖中沒有環
// 所有課程任務可以完成,應該返回 true
// 下面這個斷言一定成立,這是拓撲排序告訴我們的結論
assert stack.size() == numCourses;
int[] ret = new int[numCourses];
// 想想要怎麼得到結論,我們的 dfs 是一致將後繼結點進行 dfs 的
// 所以壓在棧底的元素,一定是那個沒有後繼課程的結點
// 那個沒有前驅的課程,一定在棧頂,所以課程學習的順序就應該是從棧頂到棧底
// 依次出棧就好了
for (int i = 0; i < numCourses; i++) {
ret[i] = stack.pop();
}
return ret;
}
/**
* 注意這個 dfs 方法的語義
*
* @param i 當前訪問的課程結點
* @param graph
* @param marked 如果 == 1 表示正在訪問中,如果 == 2 表示已經訪問完了
* @return true 表示圖中存在環,false 表示訪問過了,不用再訪問了
*/
private boolean dfs(int i,
HashSet<Integer>[] graph,
int[] marked,
Stack<Integer> stack) {
// 如果訪問過了,就不用再訪問了
if (marked[i] == 1) {
// 從正在訪問中,到正在訪問中,表示遇到了環
return true;
}
if (marked[i] == 2) {
// 表示在訪問的過程中沒有遇到環,這個節點訪問過了
return false;
}
// 走到這裡,是因為初始化呢,此時 marked[i] == 0
// 表示正在訪問中
marked[i] = 1;
// 後繼結點的集合
HashSet<Integer> successorNodes = graph[i];
for (Integer successor : successorNodes) {
if (dfs(successor, graph, marked, stack)) {
// 層層遞迴返回 true ,表示圖中存在環
return true;
}
}
// i 的所有後繼結點都訪問完了,都沒有存在環,則這個結點就可以被標記為已經訪問結束
// 狀態設定為 2
marked[i] = 2;
stack.add(i);
// false 表示圖中不存在環
return false;
}
}