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:
- The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
- You may assume that there are no duplicate edges in the input prerequisites.
題意
現在總共有n門要上的課,編號從0
到n - 1
。
有些課必須先上一些前置課程才能上,比如說要上編號為0的課,必須先完成編號為1的課程才行,這樣的關系被表示為一個pair:[0, 1]
。
給出課程總數n和一個pair的表,判斷是否有可能完成所有的課程?
分析
這個題本質上是一個判斷一張圖是否為有向無環圖(DAG)的題目。
DAG,顧名思義,如果一個有向圖中從任意點出發都不能回到該點的話,這張圖就是一個有向無環圖
課程就表示圖中的點,而前置課程的關系則表示了圖中的有向邊。需要特別注意的是,完成事件A才能繼續完成事件B,這樣的關系我們通常表示為A->B;但是在題目中,要先完成課程1才能完成課程0,這個關系被表示為了[0, 1]
,所以在代碼中構造圖的信息時,需要留意。
根據《算法概論》中對有向無環圖的講解,判斷一個有向圖是否有環,有兩個算法:
拓撲排序
即找出該圖的一個線性序列,使得需要事先完成的事件總排在之后才能完成的事件之前。如果能找到這樣一個線性序列,則該圖是一個有向無環圖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;
}
};