题目
题目链接:力扣207:课程表
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 10^5
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i]
中的所有课程对 互不相同
解题思路
这题显然是一个 拓扑排序 相关的题,只要检查是否存在环即可,存在即无法完成,不存在就可以完成。
👉递归
递归的思路就是逐个遍历数组里的每个课程,完成一个课程后再去完成下一个可以完成的课程,这里的下一个可以完成的课程可能不止一个,有可能是多个,所以需要遍历完成。如果遍历到已经完成了的课程,那就说明存在环,即返回 false
。
由于二维数组里的每个数组里存储的第二个数是需要先完成的课程,所以可以先把它的位置调换了(不调换也可以,不过思维是反向的)。然后遍历数组的每个课程到递归函数里,递归函数的参数一定是传入的课程。
函数的返回条件是存在环或者说该课程已经遍历过了。
函数的内容是遍历完成该课程后可以完成的课程,将其传入递归函数。
这里有几个细节的地方是:如样例:[[0,1],[1,2],[2,3],[4,2]]
,完成课程 1
和 4
都需要先完成课程 2
,这时,如果先遍历了课程 2
,再遍历课程 4
时会被误判成存在环。所以需要一个小处理,把没有遍历到的课程标记为 0
,刚遍历到的课程标记为 1
,遍历完当前课程完成后可以完成的课程(如完成了课程 2
后可以完成课程1
和4
)标记为2
,就可以解决。
👉迭代
迭代我觉得看一下官方的动图就能理解。
主要思路是先从入度最少的节点(课程)作为先完成的课程,然后依次遍历入度多的。可以把节点放在一个队列里,从入度少到多,逐个放入队列进行遍历。具体可以看代码。
代码(C++)
递归
class Solution {
public:
vector<vector<int>> nums;
vector<int> vis;
bool f;
void dfs(int t) {
if (vis[t])
return ;
vis[t] = 1;
for (int i = 0; i < nums[t].size(); ++ i) {
if (!vis[nums[t][i]])
dfs(nums[t][i]);
else if (vis[nums[t][i]] == 1) {
f = true;
return ;
}
}
vis[t] = 2;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
nums.resize(numCourses);
vis.resize(numCourses);
for (int i = 0; i < prerequisites.size(); ++ i) {
nums[prerequisites[i][1]].push_back(prerequisites[i][0]);
}
for (int i = 0; i < numCourses; ++ i) {
if (!vis[i])
dfs(i);
}
return !f;
}
};
迭代
class Solution {
private:
vector<vector<int>> edge;
vector<int> f;
public:
bool canFinish(int numCourses, vector<vector<int>> &prerequisites) {
edge.resize(numCourses);
f.resize(numCourses);
for (int i = 0; i < prerequisites.size(); ++i) {
edge[prerequisites[i][1]].push_back(prerequisites[i][0]);
f[prerequisites[i][0]]++;
}
queue<int> q;
int visit = 0;
for (int i = 0; i < numCourses; ++i) {
if (f[i] == 0)
q.push(i);
}
while (!q.empty()) {
visit++;
int u = q.front();
q.pop();
for (int j = 0; j < edge[u].size(); ++j) {
if (--f[edge[u][j]] == 0) {
q.push(edge[u][j]);
}
}
}
return visit == numCourses;
}
};
总结
拓扑排序的题不算太少见,应当掌握这类题。这题还算是基本或者经典的拓扑排序题,如果之前没遇到过拓扑排序的题估计不太好想,尤其是一个节点可以连接多个节点而且还不形成环。迭代做法的思路真的很好,从入度少的开始切入,这样简单了很多。