53.Course Schedule
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?
Example1
Input:
2, [ [0,1] ]
Output:
true
Explanation:
There are a total of 2 courses to take.
To take course 0 you should have finished course 1. So it is possible.
Example2
Input:
2, [ [1,0] , [0,1] ]
Output:
false
Explanation:
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.
思路
这周算法课讲的是图论里面的深度优先搜索算法(Depth-First-Search),这个题就很适合拿来进行练习。如果要用图解出这个问题的话,假设要完成课程0,需要先完成课程1,那么就有一条由0指向1的有向边,把Example1
和Example2
画成图:
2, [ [0,1] ]
2, [ [1,0] , [0,1] ]
这两个例子还不够直观,再多举两个例子:
3,[ [0,1] , [0,2] , [1,2] ](能完成)
3,[ [0,1] , [2,0] , [1,2] ](不能完成)
这样我们就能很清楚的看到,当课程中有部分课程互为前置课程,也就是有向图中存在环的时候,我们是不能完成全部课程的,所以思路就很清晰了,从一个节点出发,如果能够再次返回这个节点,那么就证明图中存在着环。下面是我的第一版代码:
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites)
{
vector<int> mark; //标记数组,记录这条路径访问过哪些节点
for (int i = 0; i < numCourses; i++)
{
mark.clear();
if (findCircle(i, prerequisites, mark, arrived)) return false;
}
return true;
}
//递归函数
bool findCircle(int num, vector<pair<int, int>>& prerequisites, vector<int>& mark)
{
for (int i = 0; i < mark.size(); i++)
if (mark[i] == num) return true; //当前节点已经存在于标记数组,证明有环
mark.push_back(num);
for (int i = 0; i < prerequisites.size(); i++)
if(prerequisites[i].first == num &&
findCircle(prerequisites[i].second, prerequisites, mark)) return true;
mark.pop_back();
return false;
}
};
这段代码应该是正确的,但提交代码到LeetCode时,不出所料,超出时间限制了。上面的第一版没有任何剪枝,对于一条n个节点的路径,会遍历n次,这就有太多没有必要的重复访问了,课程数为2000时就已经超时。我尝试加入一个最简单的剪枝来解决重复遍历,于是有了下面的第二版代码:
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites)
{
vector<int> arrived; //标记所有无环路径的节点
vector<int> mark;
int i,j;
for (i = 0; i < numCourses; i++)
{
//如果一个节点在一条无环路径上,就不需要从这个节点开始遍历寻找环
//即使这个节点可能在一个环上,也可以从环上的其他节点开始遍历,依然能够访问到这个节点
for (j = 0; j < arrived.size(); j++)
if (arrived[j] == i) break;
if (j == arrived.size())
{
mark.clear();
if (findCircle(i, prerequisites, mark, arrived)) return false;
}
}
return true;
}
bool findCircle(int num, vector<pair<int, int>>& prerequisites, vector<int>& mark, vector<int>& arrived)
{
for (int i = 0; i < mark.size(); i++)
if (mark[i] == num) return true;
mark.push_back(num);
arrived.push_back(num);
for (int i = 0; i < prerequisites.size(); i++)
if(prerequisites[i].first == num &&
findCircle(prerequisites[i].second, prerequisites, mark, arrived)) return true;
mark.pop_back();
return false;
}
};
第二版的代码虽然能够通过审核了,但能够改进的地方还很多。第一,DFS函数写的非常粗糙,很不方便阅读,理解它变得很困难。第二点就是还有很多可以进行剪枝的地方我都没有去考虑,代码的效率一般。DFS是一个很强力的算法工具,原理并不难,但应用于具体问题时非常考验人的算法能力,对于它我也只是一知半解,还有许多需要研究学习的内容,希望下次我能够写出足够优雅的DFS函数。