LeetCode#207: Course Schedule

Problem

There are a total of n courses you have to take, labeled from 0 to n - 1.
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?
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:

  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.

題意

現在總共有n門要上的課,編號從0n - 1
有些課必須先上一些前置課程才能上,比如說要上編號為0的課,必須先完成編號為1的課程才行,這樣的關系被表示為一個pair:[0, 1]
給出課程總數n和一個pair的表,判斷是否有可能完成所有的課程?

分析

這個題本質上是一個判斷一張圖是否為有向無環圖(DAG)的題目。

DAG,顧名思義,如果一個有向圖中從任意點出發都不能回到該點的話,這張圖就是一個有向無環圖
課程就表示圖中的點,而前置課程的關系則表示了圖中的有向邊。需要特別注意的是,完成事件A才能繼續完成事件B,這樣的關系我們通常表示為A->B;但是在題目中,要先完成課程1才能完成課程0,這個關系被表示為了[0, 1],所以在代碼中構造圖的信息時,需要留意。

根據《算法概論》中對有向無環圖的講解,判斷一個有向圖是否有環,有兩個算法:

  1. 拓撲排序
    即找出該圖的一個線性序列,使得需要事先完成的事件總排在之后才能完成的事件之前。如果能找到這樣一個線性序列,則該圖是一個有向無環圖

  2. DFS
    遍歷圖中的所有點,從i點出發開始DFS,如果在DFS的不斷深入過程中又回到了該點,則說明圖中存在回路。

對這道題,自己沒有得到很好的解法,代碼也不是很優雅,所以主要以Leetcode上Solution中的C++代碼為例,看看是如何實現這兩種算法的。

Code

拓撲排序

/*******************************************/
/* 類拓撲排序算法的實現
/* 說明:
/*      由于題目只要求判斷是否存在環,并不需要直接給出線性序列解
/*      所以代碼只是用了拓撲排序的思想,而不是真正實現了拓撲排序
/* 實現思路:
/*      每次從圖中去掉一個入度為0的點,去掉該點后,該點指向的點的入度-1;
/*      如果最后所有的點都被去掉了,則說明該圖是dag
/* 步驟:
/*      1. 建立一個二維出表,存放了每個點指向的點
/*      2. 初始化得到每個點的入度
/*      3. for循環次數為n次,每次遍歷整個出表,如果沒有找到入度為0的點則return false;否
/*         則記錄j,將j的入度標為-1,防止重復訪問該點
/*      4. 將j點指向的點的入度全部減一
/*      5. 開始新的循環
/*******************************************/
class Solution {
public:
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
        _iniInDegree(numCourses, prerequisites);
        _iniOutTable(numCourses, prerequisites);
        for (int i = 0; i < numCourses; i++){
            int j = 0;
            for (; j < numCourses; j++){
                if (inDegree[j] == 0)   break;
            }
            if (j == numCourses)    return false;
            inDegree[j] = -1;
            for (int k = 0; k < outTable[j].size(); k++)
                inDegree[outTable[j][k]]--;
        }
        return true;
    }
private:
    vector<int> inDegree;
    vector<vector<int>> outTable;
    void _iniInDegree(int numCourses, vector<pair<int, int>>& prerequisites){
        inDegree.resize(numCourses);
        for (int i = 0; i < numCourses; i++)
            inDegree[i] = 0;
        for (int i = 0; i < prerequisites.size(); i++)
            inDegree[prerequisites[i].first]++;
    } 
    void _iniOutTable(int numCourses, vector<pair<int, int>>& prerequisites){
        outTable.resize(numCourses);
        for (int i = 0; i < prerequisites.size(); i++)
            outTable[prerequisites[i].second].push_back(prerequisites[i].first);
    }
};

DFS

/*******************************************/
/* DFS算法的實現
/* 說明:
/*      從一點開始進行DFS,如果在DFS深入的過程中又回到了該點,則該圖存在環路;否則該圖是DAG
/* 步驟:
/*      1. 同上
/*      2. 建立兩個bool容器visited和onpath,大小均為numCourses,初始值均為false
/*      3. visited標記被訪問過的點;onpath標記某個點當前是否正在被dfs
/*      4. for循環次數為n次,每次判斷一個點:
/*         如果該點未被訪問過,則對該點進行DFS(用&&來實現)
/*         如果該點DFS的結果為true,表明從這一個點出發得到了環路(取決于DFS的返回值設定,也可以設定為false),return false
/*         如果該點DFS的結果為false,則表明沒有在該點探測到環路,return true
/*      5. DFS函數的結構
/*         先判斷當前dfs的點是否被訪問過了,如果被訪問過,return false
/*         如果沒有被訪問過,那么將該點的visited和onpath置為true
/*         遍歷該點指向的點,如果有一點正在被dfs(onpath為true),則表明存在環路,返回true;如果沒有,則對該點進行dfs
/* Note:不要把多線程的概念混進來,對于DFS而言,任意時刻只有一棵DFS樹在生長,只有當這棵樹被完全return了之后才會進行下一輪的DFS
/*******************************************/
class Solution {
public:
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
        _iniOutTable(numCourses, prerequisites);
        visited.resize(numCourses);
        onpath.resize(numCourses);
        for (int i = 0; i < numCourses; i++)
            visited[i] = onpath[i] = false;
        
        for (int i = 0; i < numCourses; i++){
            /* 在C++中,進行與運算時,如果第一個表達式的值是false,則后面的表達式都不用判斷;否則,繼續判斷下一個表達式 */
            if (!visited[i] && _dfs(i))
                return false;
        }
        return true;
    }
private:
    vector<vector<int>> outTable;
    vector<bool> visited;
    vector<bool> onpath;
    void _iniOutTable(int numCourses, vector<pair<int, int>>& prerequisites){
        outTable.resize(numCourses);
        for (int i = 0; i < prerequisites.size(); i++)
            outTable[prerequisites[i].second].push_back(prerequisites[i].first);
    }
    bool _dfs(int v){
        if (visited[v]) return false;
        visited[v] = onpath[v] = true;
        for (int i = 0; i < outTable[v].size(); i++){
            /* 在C++中,進行或運算時,如果第一個表達式的值是true,則后面的表達式都不用判斷;否則,繼續判斷下一個表達式 */
            if (onpath[outTable[v][i]] || _dfs(outTable[v][i]))
                return true;
        }
        /* return false */
        return onpath[v] = false;
    }
};
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容