力扣207——課程表

這道題主要利用拓撲排序,判斷該圖是否有環,其中還會涉及到鄰接矩陣。
<!-- more -->java

原題

如今你總共有 n 門課須要選,記爲 0 到 n-1。node

在選修某些課程以前須要一些先修課程。 例如,想要學習課程 0 ,你須要先完成課程 1 ,咱們用一個匹配來表示他們: [0,1]git

給定課程總量以及它們的先決條件,判斷是否可能完成全部課程的學習?github

示例 1:segmentfault

輸入: 2, [[1,0]] 
輸出: true
解釋: 總共有 2 門課程。學習課程 1 以前,你須要完成課程 0。因此這是可能的。

示例 2:數組

輸入: 2, [[1,0],[0,1]]
輸出: false
解釋: 總共有 2 門課程。學習課程 1 以前,你須要先完成​課程 0;而且學習課程 0 以前,你還應先完成課程 1。這是不可能的。

說明:學習

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

提示:優化

  1. 這個問題至關於查找一個循環是否存在於有向圖中。若是存在循環,則不存在拓撲排序,所以不可能選取全部課程進行學習。
  2. 經過 DFS 進行拓撲排序 - 一個關於Coursera的精彩視頻教程(21分鐘),介紹拓撲排序的基本概念。
  3. 拓撲排序也能夠經過 BFS 完成。

原題url:https://my.openwrite.cn/user/...ui

解題

這是我第一次遇到圖相關的題目,講道理,有向圖、無向圖、出度、入度之類的概念還能記得,可是拓撲排序、鄰接矩陣、逆鄰接矩陣卻只是知道有這麼一個概念,但具體內容也已經忘光了。我會在下面的解題過程當中爲你們呈現這些概念。url

先介紹一下拓撲排序:

在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的全部頂點的線性序列。且該序列必須知足下面兩個條件:

1. 每一個頂點出現且只出現一次。
2. 若存在一條從頂點 A 到頂點 B 的路徑,那麼在序列中頂點 A 出如今頂點 B 的前面。

有向無環圖(DAG)纔有拓撲排序,非DAG圖沒有拓撲排序一說。

從上面的概念中能夠看出,這道題目就是要判斷給定的圖是不是有向無環圖,也就是其是否有拓撲排序

求一個圖是否有拓撲排序,咱們通常有兩種辦法:廣度優先搜索 + 鄰接矩陣深度優先搜索 + 逆鄰接矩陣。接下來咱們逐一來爲你們分析:

廣度優先搜索 + 鄰接矩陣

首先看一下什麼是鄰接矩陣

在圖論中,鄰接矩陣(英語:adjacency matrix)是表示一種圖結構的經常使用表示方法。它用數字方陣記錄各點之間是否有邊相連,數字的大小能夠表示邊的權值大小。

這麼看有點抽象,簡單點說:就是一個圖中各個節點的後繼節點鏈表。

舉個例子:

這樣一個圖,其鄰接矩陣爲:

1 -> 2 -> 3 -> null
2 -> 4 -> null
3 -> 4 -> null
4 -> null

好了,弄懂了鄰接矩陣,咱們來想一想如何使用廣度優先搜索

假設有向圖無環,那麼從入度爲 0 的點,依次刪除,這裏並非真正意義上的刪除,只是若是該節點消失後,其後繼節點的入度須要減1,此時再判斷是否又有新的入度爲0的節點,若是最終全部節點都會被減到0,那麼說明有向圖無環,讓咱們看一下代碼:

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 利用入度表和DFS進行拓撲排序
        // 下標i對應的節點,其入度的數組
        int[] indegressArray = new int[numCourses];
        // key:節點i,value:其全部出度節點
        Map<Integer, List<Integer>> map = new HashMap<>(numCourses * 4 / 3 + 1);
        for (int[] pre : prerequisites) {
            indegressArray[pre[0]] += 1;
            List<Integer> list = map.get(pre[1]);
            if (list == null) {
                list = new LinkedList<>();
                map.put(pre[1], list);
            }
            list.add(pre[0]);
        }
        // 將入度爲0的入隊列
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < indegressArray.length; i++) {
            int indegress = indegressArray[i];
            if (indegress != 0) {
                continue;
            }
            list.add(i);
        }

        while (!list.isEmpty()) {
            // 獲取第一個節點,並將這個節點"刪除"
            int node = list.removeFirst();
            numCourses--;
            // 那麼以node做爲前驅節點的節點,其入度-1
            List<Integer> preList = map.get(node);
            if (preList == null) {
                continue;
            }
            for (Integer suffixNode : preList) {
                indegressArray[suffixNode] -= 1;
                // 若是該節點入度減爲0,則也入隊
                if (indegressArray[suffixNode] == 0) {
                    list.add(suffixNode);
                }
            }
        }

        // 若是最終全部節點都入隊而且也出隊,那麼說明該圖無環。
        return numCourses == 0;
    }
}

提交OK,執行用時:8 ms,內存消耗:45 MB,執行用時只打敗了84.74%的 java 提交記錄,咱們再優化優化試試。

廣度優先搜索 + 鄰接矩陣 優化

map 雖然理論上查找速度爲 O(1),但須要先計算 hash 值,而數組的話,其獲取地址是根據下標的。而咱們這裏的數字是連續的,而且從 0 開始,所以很適用數組的狀況,所以作一個改造:

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 利用入度表DFS進行拓撲排序
        // 下標i對應的節點,其入度的數組
        int[] indegressArray = new int[numCourses];
        // 鄰接矩陣,數組下標:節點i,list:其全部後繼節點
        List<Integer>[] adjacencyMatrix = new LinkedList[numCourses];
        for (int[] pre : prerequisites) {
            // 入度表相應節點的入度+1
            indegressArray[pre[0]] += 1;
            // 鄰接矩陣
            List<Integer> list = adjacencyMatrix[pre[1]];
            if (list == null) {
                list = new LinkedList<>();
                adjacencyMatrix[pre[1]] = list;
            }
            list.add(pre[0]);
        }
        // 將入度爲0的入隊列
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < indegressArray.length; i++) {
            int indegress = indegressArray[i];
            if (indegress != 0) {
                continue;
            }
            list.add(i);
        }

        while (!list.isEmpty()) {
            // 獲取第一個節點,並將這個節點"刪除"
            int node = list.removeFirst();
            numCourses--;
            // 那麼以node做爲前驅節點的後繼節點,入度-1
            List<Integer> preList = adjacencyMatrix[node];
            if (preList == null) {
                continue;
            }
            for (Integer prefixNode : preList) {
                indegressArray[prefixNode] -= 1;
                // 若是該節點入度減爲0,則也入隊
                if (indegressArray[prefixNode] == 0) {
                    list.add(prefixNode);
                }
            }
        }

        // 若是最終全部節點都入隊而且也出隊,那麼說明該圖無環。
        return numCourses == 0;
    }
}

提交OK,執行用時:5 ms,內存消耗:45 MB,執行用時打敗了94.01%的 java 提交記錄,應該差很少了。

深度優先搜索

既然知道了鄰接矩陣,那麼逆鏈接矩陣就是指的各個節點的前驅節點鏈表。仍是以以前的那個例子:

這樣一個圖,其逆逆鄰接矩陣爲:

1 -> null
2 -> 1 -> null
3 -> 1 -> null
4 -> 2 -> 3 -> null

那麼如何進行深度優先遍歷呢?也就是以一個節點出發,訪問其相鄰節點,一直遍歷下去,若是發現一個節點被訪問兩次,說明有環,那麼返回失敗,不然就標記該節點已經所有訪問完成。當訪問徹底部節點成功後,說明有向圖無環。

這麼一看,深度優先遍歷的時候,其實只要保證相鄰便可,無所謂鄰接矩陣仍是逆鄰接矩陣。

咱們來看看代碼:

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 利用逆鄰接矩陣,進行深度優先搜索
        HashSet<Integer>[] inadjacencyMatrix = new HashSet[numCourses];
        for (int i = 0; i < numCourses; i++) {
            inadjacencyMatrix[i] = new HashSet<>();
        }
        // 構造逆鏈接矩陣(每一個節點的全部後繼節點)
        for (int[] array : prerequisites) {
            inadjacencyMatrix[array[1]].add(array[0]);
        }

        // 全部節點的被使用狀況,若是正在使用的節點,再次被訪問,說明有環
        // 0:未使用;1:正在使用;2:已經使用完成;
        int[] used = new int[numCourses];

        for (int i = 0; i < numCourses; i++) {
            if (dfs(i, inadjacencyMatrix, used)) {
                return false;
            }
        }

        return true;
    }

    public boolean dfs(int index, HashSet<Integer>[] inadjacencyMatrix, int[] used) {
        // 若是當前節點已經處於正在被訪問的狀態,如今又再次訪問,說明有環
        if (used[index] == 1) {
            return true;
        }
        // 若是當前節點已經訪問結束,則能夠不用再次被訪問
        if (used[index] == 2) {
            return false;
        }

        // used[index] == 0,說明該節點從未被訪問過,那麼如今開始訪問該節點
        used[index] = 1;
        // 深度遍歷該節點的後繼節點
        HashSet<Integer> suffixNodes = inadjacencyMatrix[index];
        for (int suffixNode : suffixNodes) {
            if (dfs(suffixNode, inadjacencyMatrix, used)) {
                return true;
            }
        }
        // 深度遍歷完成該節點,直接結束
        used[index] = 2;
        return false;
    }
}

提交OK,執行用時:8 ms,內存消耗:44.6 MB,優化的話,暫時並無想到好方法。

總結

以上就是這道題目個人解答過程了,不知道你們是否理解了。這也是我第一次解決圖相關的題目,涉及的知識點有些多,須要好好消化。

有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。

https://death00.github.io/

公衆號:健程之道

相關文章
相關標籤/搜索