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.
题意:从0~n-1的n门课程,有优先次序,比如你要上课程0,必须先上课程1,用pair:[0,1]来表示优先次序。问你能否完成所有课程。
样例:2 [[1,0]]
给出两门课,上课程1必须先上课程0,输出true
样例:2 [[1,0],[0,1]]
上课程1必须先上课程0,上课程0又必须先上课程1,输出false
解题思路:很显然是拓扑排序的题,我们可以根据所有pair,建立图
一种方式可以判断存在不存在环,但是不能输出上课次序。
另一种方式可以进行拓扑排序,输出上课次序与否可以根据我们需要。
1、Approach One
拓扑排序(不清楚AOV和拓扑排序的需要自行了解)
class Solution {
public:
/*
* 1. 如何解决这个问题?
* 先建立AOV网,选择邻接表法建图,之后进行拓扑排序。
* 2. 理清拓扑排序思路:
* (1) 从AOV网中选择一个没有前驱的顶点,输出它
* (2) 从AOV网中删除该顶点,并且删除以该顶点为尾的全部有向边
* (3) 重复上述两步,直到剩余的网中不再存在没有前驱的顶点为止
* 3. 如何建图?使用什么数据结构?要注意什么?
* 数据结构:根据题目给出条件,我们可以使用vector<unordered_set<int>> graph建图
* 注意:由于要进行拓扑排序,我们建树时要记录每个节点的入度vector<int> indegrees(numCourses, 0)
* 建图:for pre in prerequisites: // <0,1> 1 -> 0 : to take course 0 you have to first take course 1
* graph[pre.second].insert(pre.first) // 插入节点
* indegrees[pre.first]++; // 弧尾入度++
* 4. 拓扑排序
* 此问题不需要进行边的删除,我们只需要减少节点的入度就可以了
*/
/* Approach One:拓扑排序不使用栈或者队列
* 属于BFS:访问一个顶点,然后访问该顶点的其余邻接点 */
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_set<int>> graph(numCourses); // 模拟邻接表
vector<int> indegrees(numCourses, 0); // 用于记录节点入度数
createGraph(graph, numCourses, prerequisites, indegrees);
// 拓扑排序
for(int i = 0; i < numCourses; ++i){
// 找到入度为0的节点
int zeroIn = 0;
for(; zeroIn < numCourses; ++zeroIn)
if(!indegrees[zeroIn]) break;
// 没有入度为0的节点了
if(zeroIn == numCourses) return false;
// 删除顶点和与顶点相连的边
// 如 1 -> 2 , 1 -> 3 :1的入度变为-1,而2、3的入度均要减1
indegrees[zeroIn] = -1;
for(int next : graph[zeroIn])
indegrees[next]--;
}
return true;
}
void createGraph(vector<unordered_set<int>> &graph, int numCourses
, vector<pair<int, int>>& prerequisites, vector<int>& indegrees){
for(pair<int, int>& pre : prerequisites){
graph[pre.second].insert(pre.first); // 插入节点
indegrees[pre.first]++; // 弧尾入度++
}
}
};
分析时间复杂度和空间复杂度
Time Complexity:建图O(n),拓扑排序O(n^2),拓扑排序没有使用队列和栈,所以总的时间复杂度是O(n^2)
Space Complexity:邻接表O(n+e),n代表顶点数,e代表边数
提交结果:beats 41%
2. Approach Two
拓扑排序使用栈:
由于上述代码中,我们每次都需要遍历indegrees中重新找出入度为0的顶点,我们可以使用栈或者队列(自己思考为什么都可以),此处我们使用栈:
1. 先将所有入度为0的顶点入栈
2. 循环从弹出栈顶元素,每次删除栈顶元素的边(即减少弧头的入度, 1 -> 2,2是弧头,indegrees[2]–)时,如果弧头的入度变成了0,将弧头加入栈,之后继续弹出,直至栈空
3. 使用栈还存在一个问题,如果是非连通图,那么我们没有遍历到所有的课程,不能完成所有课程。这个问题我们可以使用一个计数器来计算出栈的元素个数。
注意:如果存在环,会有入度不为0的节点,这样便解决了我们的问题。
/* Approach Two:拓扑排序使用栈
* BFS:访问一个顶点,然后访问该顶点的其余邻接点 */
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_set<int>> graph(numCourses); // 模拟邻接表
vector<int> indegrees(numCourses, 0); // 用于记录节点入度数
createGraph(graph, numCourses, prerequisites, indegrees);
// 拓扑排序
stack<int> st;
int counter = 0; // 记录入度为0的节点数
// 入度为0的顶点入栈
for(int i = 0; i < numCourses; ++i)
if(!indegrees[i]) st.push(i);
while(!st.empty()){
int zeroIn = st.top(); // 弹出一个入度为0的顶点
st.pop();
++counter; // 入度为0的节点数
/* plan[k++] = zeroIn; // 可以记录课程的安排次序*/
// 访问zeroIn的所有边
for(int next : graph[zeroIn]){
if(!(--indegrees[next])) // 弧头入度数减1变成了入度为0的顶点 (如1 -> 2,2即弧头)
st.push(next);
}
}
if(counter == numCourses) // 拓扑排序之后所有节点都可以入度为0,除非是非连通图或者存在环
return true;
else
return false;
}
分析时间复杂度和空间复杂度:
Time Complexity:建图O(n),第一次入栈O(n),注意到我们访问每个节点,之后都是访问该节点的边,所以时间复杂度为O(3n+e),实际拓扑排序是O(2n+e)。哇,其实如果图节点很多边较少,相对于O(n^2),是不是会有很大改进呢?
Space Complexity:邻接表O(n+e),栈O(n)
提交结果:beats 67.16%(哈哈,进步)
3、Approach Three
判断是否存在环
class Solution {
public:
/* Approach Three:查看是否存在环
* Depth First Search:
* 1. 访问一个顶点w,然后访问该顶点的未被访问的下一个邻接点v
* 2. 访问v的下一个未被访问的邻接点x,依次类推,直到所有和w相通的点都被访问过
* 3. 若此时仍有顶点未被访问,则从中选一个顶点做为始点,重复1.2
* 注意:我们要使用一个visited[0..n-1],标识顶点是否被访问
* 那么在有向图的DFS中,我们如何判断是否存在环呢?
* 很简单,我们可以记录我们访问的路径path
* 假设我们正在访问V,准备访问V的下一个邻接点W,但是W在我们访问的路径上,则存在环
*/
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
// 模拟邻接表
vector<unordered_set<int>> graph = createGraph(numCourses, prerequisites);
vector<bool> visited(numCourses, false); // 被访问过的节点
vector<bool> path(numCourses, false); // 当前的访问路径
for(int i = 0; i < numCourses; ++i)
if(!visited[i] && dfsCycle(graph, i, visited, path)) // 顶点未被访问过,但存在环
return false;
return true;
}
bool dfsCycle(vector<unordered_set<int>>& graph, int v, vector<bool>& visited, vector<bool>& path){
if(visited[v]) return false; // 被访问过的节点不需要再次被访问
path[v] = visited[v] = true; // 设置顶点为被访问过,加入当前的访问路径
for(int next : graph[v]){
// 如果该顶点的邻接点在当前访问路径上,说明存在环,否则查看邻接点
if(path[next] || dfsCycle(graph, next, visited, path))
return true;
}
path[v] = false; // 递归回来时,我们需要将该顶点取出访问路径
return false;
}
vector<unordered_set<int>> createGraph(int numCourses, vector<pair<int, int>>& prerequisites){
vector<unordered_set<int>> graph(numCourses);
for(pair<int, int>& pre : prerequisites)
graph[pre.second].insert(pre.first); // 插入节点
return graph;
}
};
提交结果:16ms beats 67.16%
(发现写代码之前把思路厘清是很必要的,这几次写的代码都是直接过的,惊讶到自己,哈哈)