(一)问题描述
你这个学期必须选修 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 <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i]
中的所有课程对 互不相同
(二)解决思路
显然应该用图数据结构来理解这道题。prerequisites[[1,0]]代表学习课程1之前需要完成课程0,实际相当于节点1和0,存在关系1-->0,是一个有向图,这道题的思路就是:如果某个课程没有前置课程,或者该课程的前置课程确定都可以上,那么该课程也就可以上。如果可以上的课程数等于总课程数,就返回true,否则返回false。
- 对于每个节点,统计其前置课程数,存储在数组inedge中,数组的下标就是课程;
- 对于每个节点,统计以该课程作为前置课程的所有课程,保存在二维数组edges中,edges的第一维下标代表课程,第二维下标代表以该课程为前置课程的课程;
- 遍历整个inedge数组,找到是否有某个课程的前置课程为0,这个课程就是图在拓扑意义上的头节点,这个课程就是现在可以上的课程(没有前置),将这个课程加入到队列中;
- 取出队列中的头节点u,找哪些课程是以这个课程为前置课程的,即edge[u],遍历edge[u]的每一个课程v。课程u现在可以上了,那么inedge[v]就可以减1。如果减掉1之后,inedge==0,说明课程v也可以上了,把它也加入到队列中;
- 用visited来记录可以上的课程数,如果最后visited的数量等于给定课程数numCourses,就说明所有课程都可以上,返回true,否则返回false。
这道题的思路也相当于验证图是否存在环,相当于每一步删除访问到的节点的所有入边。没有出边的节点才能够加入队列。如果图中存在环,那么一定会在某一步找不到完全没有出边的节点,队列为空,导致visited != numCourses。(如果想不清楚可以画个带环的图!按照这个步骤走一遍就明白了!)
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> edges = new ArrayList<>();
int[] inedge = new int[numCourses];
for(int i = 0; i<numCourses; i++){
edges.add(new ArrayList<Integer>());
}
for(int[] pre : prerequisites){
//所有以节点pre[1]为前置课程的课程(pre[0]->pre[1])
edges.get(pre[1]).add(pre[0]);
//pre[0]的前置课程有多少个
inedge[pre[0]]++;
}
Queue<Integer> que = new LinkedList<>();
for(int i=0 ; i<numCourses; i++){
//前置课程数为0,加入队列
if(inedge[i]==0) que.add(i);
}
int visited = 0;
while(!que.isEmpty()){
//前置课程数为0的节点(一定可以上)
int u = que.poll();
visited++;
//以节点u为前置课程的课程(指向节点u的节点)
for(int v : edges.get(u)){
//取消节点v和节点u的连边
--inedge[v];
if(inedge[v]==0) que.add(v);
}
}
return visited == numCourses;
}
}
(三)易错点
在200. 岛屿数量这道题的广搜法解法里,我们强调了要在节点放进队列时就将其visited值修改为true,不要在弹出的时候才修改,会导致某些节点被重复添加到队列。
但是在这道题里,在加入时修改和在弹出时修改都可以。如果想在加入时修改,记得在找拓扑起点那一步,即最开始向队列加入第一批节点时也要visited++。