1. 程式人生 > >LeetCode 題解之 210. Course Schedule II(拓撲排序模板題 2 )

LeetCode 題解之 210. Course Schedule II(拓撲排序模板題 2 )

210. Course Schedule II

題目描述和難度

  • 題目描述:

現在你總共有 n 門課需要選,記為 0n-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]

說明:

  1. 輸入的先決條件是由邊緣列表表示的圖形,而不是鄰接矩陣。詳情請參見圖的表示法
  2. 你可以假定輸入的先決條件中沒有重複的邊。

提示:

  1. 這個問題相當於查詢一個迴圈是否存在於有向圖中。如果存在迴圈,則不存在拓撲排序,因此不可能選取所有課程進行學習。
  2. 通過 DFS 進行拓撲排序 - 一個關於Coursera的精彩視訊教程(21分鐘),介紹拓撲排序的基本概念。
  3. 拓撲排序也可以通過 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;
    }
}