【圖的遍歷】判斷可否完成課程(是否又環) Course Schedule

問題:java

There are a total of n courses you have to take, labeled from 0 to n - 1.node

Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]算法

Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?express

For example:數組

2, [[1,0]]

There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.數據結構

2, [[1,0],[0,1]]

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.機器學習

Note:ide

  1. The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
  2. You may assume that there are no duplicate edges in the input prerequisites.

Hints:學習

  1. This problem is equivalent to finding if a cycle exists in a directed graph(在有向圖中檢測環). If a cycle exists, no topological ordering exists and therefore it will be impossible to take all courses.
  2. Topological Sort via DFS - A great video tutorial (21 minutes) on Coursera explaining the basic concepts of Topological Sort.
  3. Topological sort could also be done via BFS.

解決:ui

【解析】求Course Schedule,等同問題是有向圖檢測環,vertex是course, edge是prerequisite。我以爲通常會使用Topological Sorting拓撲排序來檢測。一個有向圖假若有環則不存在Topological Order。一個DAG的Topological Order能夠有大於1種。 

 ① BFS的解法,咱們定義二維數組graph來表示這個有向圖,一位數組in來表示每一個頂點的入度。咱們開始先根據輸入來創建這個有向圖,並將入度數組也初始化好。而後咱們定義一個queue變量,將全部入度爲0的點放入隊列中,而後開始遍歷隊列,從graph裏遍歷其鏈接的點,每到達一個新節點,將其入度減一,若是此時該點入度爲0,則放入隊列末尾。直到遍歷完隊列中全部的值,若此時還有節點的入度不爲0,則說明環存在,返回false,反之則返回true。

class Solution {//30ms
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int len = prerequisites.length;
        if (numCourses == 0 || len == 0) {
            return true;
        }
        int[] in = new int[numCourses];//記錄節點的入度
        for (int i = 0;i < len;i ++){//遍歷有向邊,初始化入度
            in[prerequisites[i][0]] ++;
        }
        Queue<Integer> queue = new LinkedList<>();//保存入度爲0的節點
        for (int i = 0;i < numCourses;i ++){
            if (in[i] == 0){
                queue.offer(i);
            }
        }
        int count = queue.size();//記錄入度爲0的節點數
        while(! queue.isEmpty()){
            int top = queue.poll();
            for (int[] tmp : prerequisites ) {//頂節點指向的節點的入度-1
                if (tmp[1] == top) {
                    in[tmp[0]] --;
                    if (in[tmp[0]] == 0) {
                        count ++;
                        queue.offer(tmp[0]);
                    }
                }
            }
        }
        return count == numCourses;
    }
}

② DFS方法,遞歸判斷是否存在環,若存在,則返回false,不然,返回true。

class Solution{ // 13 ms
    public boolean canFinish(int numCourses,int[][] pre){
        if (numCourses == 0 || pre.length == 0) {
            return true;
        }
        boolean[] isvisted = new boolean[numCourses];//記錄節點的狀態
        boolean[] isonstack = new boolean[numCourses];
        Map<Integer,List<Integer>> adjacent = new HashMap<>();//保存依賴於key課程的課程的鏈表
        for (int[] tmp : pre) {
            if (adjacent.containsKey(tmp[1])) {
                adjacent.get(tmp[1]).add(tmp[0]);
            }else{
                List<Integer> list = new ArrayList<>();
                list.add(tmp[0]);
                adjacent.put(tmp[1],list);
            }
        }
        for (int i = 0;i < numCourses ;i ++ ) {
            if (dfs(adjacent,isvisted,isonstack,i)) {//存在環
                return false;
            }
        }
        return true;
    }
    public boolean dfs(Map<Integer,List<Integer>> adjacent,boolean[] isvisited,boolean[] isonstack,int i){
        isvisited[i] = true;
        isonstack[i] = true;
        if (adjacent.containsKey(i)) {
            for (int j : adjacent.get(i)) {
                if (! isvisited[j] && dfs(adjacent,isvisited,isonstack,j)) {
                    return true;
                }
                if (isonstack[j]) {
                    return true;
                }
            }
        }
        isonstack[i] = false;
        return false;
    }
}

③ 在discuss中看到的。

class Solution {//1ms
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        int[] root = new int[numCourses];//使用數組保存節點對應的前綴
        Arrays.fill(root,-1);
        for (int[] tmp : prerequisites) {
            int cur = tmp[1];
            while(cur >= 0){
                cur = root[cur];
                if (cur == tmp[0]) {//存在環
                    return false;
                }
            }
            root[tmp[0]] = tmp[1];
        }
        return true;
    }
}

【注---拓撲排序】http://blog.csdn.net/dm_vincent/article/details/7714519

一、定義:將有向圖中的頂點以線性方式進行排序。即對於任何鏈接自頂點u到頂點v的有向邊uv,在最後的排序結果中,頂點u老是在頂點v的前面

若是這個概念還略顯抽象的話,那麼不妨考慮一個很是很是經典的例子——選課。我想任何看過數據結構相關書籍的同窗都知道它吧。假設我很是想學習一門機器學習的課程,可是在修這麼課程以前,咱們必需要學習一些基礎課程,好比計算機科學概論,C語言程序設計,數據結構,算法等等。那麼這個制定選修課程順序的過程,實際上就是一個拓撲排序的過程,每門課程至關於有向圖中的一個頂點,而鏈接頂點之間的有向邊就是課程學習的前後關係。只不過這個過程不是那麼複雜,從而很天然的在咱們的大腦中完成了。將這個過程以算法的形式描述出來的結果,就是拓撲排序。

那麼是否是全部的有向圖都可以被拓撲排序呢?顯然不是。繼續考慮上面的例子,若是告訴你在選修計算機科學概論這門課以前須要你先學習機器學習,你是否是會被弄糊塗?在這種狀況下,就沒法進行拓撲排序,由於它中間存在互相依賴的關係,從而沒法肯定誰先誰後。在有向圖中,這種狀況被描述爲存在環路。所以,一個有向圖能被拓撲排序的充要條件就是它是一個有向無環圖(DAG:Directed Acyclic Graph)

二、偏序/全序關係:

偏序和全序其實是離散數學中的概念。

仍是以上面選課的例子來描述這兩個概念。假設咱們在學習完了算法這門課後,能夠選修機器學習或者計算機圖形學。這個或者表示,學習機器學習和計算機圖形學這兩門課之間沒有特定的前後順序。所以,在咱們全部能夠選擇的課程中,任意兩門課程之間的關係要麼是肯定的(即擁有前後關係),要麼是不肯定的(即沒有前後關係),絕對不存在互相矛盾的關係(即環路)。以上就是偏序的意義,抽象而言,有向圖中兩個頂點之間不存在環路,至於連通與否,是無所謂的。因此,有向無環圖必然是知足偏序關係的。

理解了偏序的概念,那麼全序就好辦了。所謂全序,就是在偏序的基礎之上,有向無環圖中的任意一對頂點還須要有明確的關係(反映在圖中,就是單向連通的關係,注意不能雙向連通,那就成環了)。可見,全序就是偏序的一種特殊狀況。回到咱們的選課例子中,若是機器學習須要在學習了計算機圖形學以後才能學習(可能學的是圖形學領域相關的機器學習算法……),那麼它們之間也就存在了肯定的前後順序,本來的偏序關係就變成了全序關係。

實際上,不少地方都存在偏序和全序的概念。

好比對若干互不相等的整數進行排序,最後老是可以獲得惟一的排序結果(從小到大,下同)。這個結論應該不會有人表示疑問吧:)可是若是咱們以偏序/全序的角度來考慮一下這個再天然不過的問題,可能就會有別的體會了。

那麼如何用偏序/全序來解釋排序結果的惟一性呢?

咱們知道不一樣整數之間的大小關係是肯定的,即1老是小於4的,不會有人說1大於或者等於4吧。這就是說,這個序列是知足全序關係的。而對於擁有全序關係的結構(如擁有不一樣整數的數組),在其線性化(排序)以後的結果必然是惟一的。對於排序的算法,咱們評價指標之一是看該排序算法是否穩定,即值相同的元素的排序結果是否和出現的順序一致。好比,咱們說快速排序是不穩定的,這是由於最後的快排結果中相同元素的出現順序和排序前不一致了。若是用偏序的概念能夠這樣解釋這一現象:相同值的元素之間的關係是沒法肯定的。所以它們在最終的結果中的出現順序能夠是任意的。而對於諸如插入排序這種穩定性排序,它們對於值相同的元素,還有一個潛在的比較方式,即比較它們的出現順序,出現靠前的元素大於出現後出現的元素。所以經過這一潛在的比較,將偏序關係轉換爲了全序關係,從而保證告終果的惟一性。

拓展到拓撲排序中,結果具備惟一性的條件也是其全部頂點之間都具備全序關係。若是沒有這一層全序關係,那麼拓撲排序的結果也就不是惟一的了。在後面會談到,若是拓撲排序的結果惟一,那麼該拓撲排序的結果同時也表明了一條哈密頓路徑。

三、典型實現算法

(1)Kahn算法:

摘一段維基百科上關於Kahn算法的僞碼描述:

L← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
while S is non-empty do
    remove a node n from S
    insert n into L
    foreach node m with an edge e from nto m do
        remove edge e from thegraph
        ifm has no other incoming edges then
            insert m into S
if graph has edges then
    return error (graph has at least onecycle)
else 
    return L (a topologically sortedorder)

不難看出該算法的實現十分直觀,關鍵在於須要維護一個入度爲0的頂點的集合

每次從該集合中取出(沒有特殊的取出規則,隨機取出也行,使用隊列/棧也行,下同)一個頂點,將該頂點放入保存結果的List中。

緊接着循環遍歷由該頂點引出的全部邊,從圖中移除這條邊,同時獲取該邊的另一個頂點,若是該頂點的入度在減去本條邊以後爲0,那麼也將這個頂點放到入度爲0的集合中。而後繼續從集合中取出一個頂點…………

當集合爲空以後,檢查圖中是否還存在任何邊,若是存在的話,說明圖中至少存在一條環路。不存在的話則返回結果List,此List中的順序就是對圖進行拓撲排序的結果。

對上圖進行拓撲排序的結果:

2->8->0->3->7->1->5->6->9->4->11->10->12

複雜度分析:

初始化入度爲0的集合須要遍歷整張圖,檢查每一個節點和每條邊,所以複雜度爲O(E+V);

而後對該集合進行操做,又須要遍歷整張圖中的,每條邊,複雜度也爲O(E+V);

所以Kahn算法的複雜度即爲O(E+V)。

(2)基於DFS的拓撲排序:

除了使用上面直觀的Kahn算法以外,還可以藉助深度優先遍從來實現拓撲排序。這個時候須要使用到棧結構來記錄拓撲排序的結果

一樣摘錄一段維基百科上的僞碼:

L ← Empty list that will contain the sorted nodes
S ← Set of all nodes with no outgoing edges
for each node n in S do
    visit(n) 
function visit(node n)
    if n has not been visited yet then
        mark n as visited
        for each node m with an edgefrom m to ndo
            visit(m)
        add n to L

DFS的實現更加簡單直觀,使用遞歸實現。利用DFS實現拓撲排序,實際上只須要添加一行代碼,即上面僞碼中的最後一行:add n to L

須要注意的是,將頂點添加到結果List中的時機是在visit方法即將退出之時

這個算法的實現很是簡單,可是要理解的話就相對複雜一點。

關鍵在於爲何在visit方法的最後將該頂點添加到一個集合中,就能保證這個集合就是拓撲排序的結果呢?

由於添加頂點到集合中的時機是在dfs方法即將退出之時,而dfs方法自己是個遞歸方法,只要當前頂點還存在邊指向其它任何頂點,它就會遞歸調用dfs方法,而不會退出。所以,退出dfs方法,意味着當前頂點沒有指向其它頂點的邊了,即當前頂點是一條路徑上的最後一個頂點。

下面簡單證實一下它的正確性:

考慮任意的邊v->w,當調用dfs(v)的時候,有以下三種狀況:

  1. dfs(w)尚未被調用,即w尚未被mark,此時會調用dfs(w),而後當dfs(w)返回以後,dfs(v)纔會返回
  2. dfs(w)已經被調用並返回了,即w已經被mark
  3. dfs(w)已經被調用可是在此時調用dfs(v)的時候還未返回

須要注意的是,以上第三種狀況在拓撲排序的場景下是不可能發生的,由於若是狀況3是合法的話,就表示存在一條由w到v的路徑。而如今咱們的前提條件是由v到w有一條邊,這就致使咱們的圖中存在環路,從而該圖就不是一個有向無環圖(DAG),而咱們已經知道,非有向無環圖是不能被拓撲排序的。

那麼考慮前兩種狀況,不管是狀況1仍是狀況2,w都會先於v被添加到結果列表中。因此邊v->w老是由結果集中後出現的頂點指向先出現的頂點。爲了讓結果更天然一些,可使用棧來做爲存儲最終結果的數據結構,從而可以保證邊v->w老是由結果集中先出現的頂點指向後出現的頂點。

複雜度分析:

複雜度同DFS一致,即O(E+V)。具體而言,首先須要保證圖是有向無環圖,判斷圖是DAG可使用基於DFS的算法,複雜度爲O(E+V),然後面的拓撲排序也是依賴於DFS,複雜度爲O(E+V)

仍是對上文中的那張有向圖進行拓撲排序,只不過此次使用的是基於DFS的算法,結果是:

8->7->2->3->0->6->9->10->11->12->1->5->4

(3)兩種實現算法的總結:

這兩種算法分別使用鏈表和棧來表示結果集。

對於基於DFS的算法,加入結果集的條件是:頂點的出度爲0。這個條件和Kahn算法中入度爲0的頂點集合彷佛有着殊途同歸之妙,這兩種算法的思想猶如一枚硬幣的兩面,看似矛盾,實則否則。一個是從入度的角度來構造結果集,另外一個則是從出度的角度來構造。

實現上的一些不一樣之處:

Kahn算法不須要檢測圖爲DAG,若是圖爲DAG,那麼在出度爲0的集合爲空以後,圖中還存在沒有被移除的邊,這就說明了圖中存在環路。而基於DFS的算法須要首先肯定圖爲DAG,固然也可以作出適當調整,讓環路的檢測和拓撲排序同時進行,畢竟環路檢測也可以在DFS的基礎上進行。

兩者的複雜度均爲O(V+E)。

四、拓撲排序解的惟一性:

哈密頓路徑:

哈密頓路徑是指一條可以對圖中全部頂點正好訪問一次的路徑。本文中只會解釋一些哈密頓路徑和拓撲排序的關係,至於哈密頓路徑的具體定義以及應用,能夠參見本文開篇給出的連接。

前面說過,當一個DAG中的任何兩個頂點之間都存在能夠肯定的前後關係時,對該DAG進行拓撲排序的解是惟一的。這是由於它們造成了全序的關係,而對存在全序關係的結構進行線性化以後的結果必然是惟一的(好比對一批整數使用穩定的排序算法進行排序的結果必然就是惟一的)。

須要注意的是,非DAG也是可以含有哈密頓路徑的,爲了利用拓撲排序來實現判斷,因此這裏討論的主要是判斷DAG中是否含有哈密頓路徑的算法,所以下文中的圖指代的都是DAG。

那麼知道了哈密頓路徑和拓撲排序的關係,咱們如何快速檢測一張圖是否存在哈密頓路徑呢?

根據前面的討論,是否存在哈密頓路徑的關鍵,就是肯定圖中的頂點是否存在全序的關係,而全序的關鍵,就是任意一對頂點之間都是可以肯定前後關係的。所以,咱們可以設計一個算法,用來遍歷頂點集中的每一對頂點,而後檢查它們之間是否存在前後關係,若是全部的頂點對有前後關係,那麼該圖的頂點集就存在全序關係,即圖中存在哈密頓路徑。

可是很顯然,這樣的算法十分低效。對於大規模的頂點集,是沒法應用這種解決方案的。一般一個低效的解決辦法,十有八九是由於沒有抓住現有問題的一些特徵而致使的。所以咱們回過頭來再看看這個問題,有什麼特徵使咱們沒有利用的。仍是舉對整數進行排序的例子:

好比如今有3, 2, 1三個整數,咱們要對它們進行排序,按照以前的思想,咱們分別對(1,2),(2,3),(1,3)進行比較,這樣須要三次比較,可是咱們很清楚,1和3的那次比較其實是多餘的。咱們爲何知道此次比較是多餘的呢?我認爲,是咱們下意識的利用了整數比較知足傳遞性的這一規則。可是計算機是沒法下意識的使用傳遞性的,所以只能經過其它的方式來告訴計算機,有一些比較是沒必要要的。因此,也就有了相對插入排序,選擇排序更加高效的排序算法,好比歸併排序,快速排序等,將n2的算法加速到了nlogn。或者是利用了問題的特色,採起了更加獨特的解決方案,好比基數排序等。

扯遠了一點,回到正題。如今咱們沒有利用到的就是全序關係中傳遞性這一規則。如何利用它呢,最簡單的想法每每就是最實用的,咱們仍是選擇排序,排序後對每對相鄰元素進行檢測不就間接利用了傳遞性這一規則嘛?因此,咱們先使用拓撲排序對圖中的頂點進行排序。排序後,對每對相鄰頂點進行檢測,看看是否存在前後關係,若是每對相鄰頂點都存在着一致的前後關係(在有向圖中,這種前後關係以有向邊的形式體現,即查看相鄰頂點對之間是否存在有向邊)。那麼就能夠肯定該圖中存在哈密頓路徑了,反之則不存在。

相關文章
相關標籤/搜索