這道題主要利用拓撲排序,判斷該圖是否有環,其中還會涉及到鄰接矩陣。
<!-- 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。這是不可能的。
說明:學習
提示:優化
原題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
,優化的話,暫時並無想到好方法。
以上就是這道題目個人解答過程了,不知道你們是否理解了。這也是我第一次解決圖相關的題目,涉及的知識點有些多,須要好好消化。
有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。
公衆號:健程之道