1. 问题描述:
你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]
给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
提示:
输入的先决条件是由 边缘列表 表示的图形,而不是 邻接矩阵 。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
1 <= numCourses <= 10^5
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule
2. 思路分析:
① 首先需要读懂题目,需要搞清楚题目本质要我们使用什么方法来求解,通过分析我们可以这样想,将所有的课程看成一个点,假如B是A的先修课的话那么在AB之间连接上一条A指向B的线,对于其他的课程也是这样的,这样就可以将其转化为图论的知识来解决,通过画图可以知道假如给出的所有课程符合逻辑的话那么图就一定没有环的,假如存在不符合逻辑的某些顶点的话那么肯定是存在环的,因为在先要修完其他的课,但是其他的课又要依赖于自己这个是不可能的所以存在环,很明显这个是典型的拓扑排序题目,本质上没有任何的区别,所以我们可以使用拓扑排序的知识来解决即可
② 使用拓扑排序,关键有以下几个步骤:
1)根据题目的节点之间的关系来存储所有节点各自的关系,也就是哪些节点之间是存在着边的
2)使用一个数组来记录各个顶点的入度,在一开始的循环中根据顶点之间的关系计算出顶点的入度
3)在循环中统计入度为零的顶点,加入到一个辅助的队列中,为什么要使用队列呢?因为队列中可能存在不止一个入度为0的顶点,使用栈或者是List也是可以的,只是常用的是队列
4)当队列不为空的时候,弹出队列的一个节点,并且将该节点所有连接的边都删除掉,这对应着删掉连接的顶点的入度的操作,统计入度为零的顶点并且将其加入到队列中,每弹出一个节点那么统计一次最后看统计的次数与所有节点的数量是否相等,假如相等那么肯定是没有环的
之前也写过一篇拓扑排序的博客
③ 对于这道题目来说,使用Map<Integer, List<Integer>>来映射顶点之间的关系,键为当前的课程,值为先修课程,因为值可能存在多个所以需要设置为List<Integer>,这样通过map就可以表示节点之间的关系了,在将节点的关系加入到map中需要注意空指针异常,使用map中的getOrDefault方法就可以不用判断当前获取的键是否为空了比较方便,因为在获取的时候可以为空的时候可以设置默认值,然后再将其键值对设置在map中,同时在检查当前顶点是否有其他的边的是否,使用map也可能出现异常因为在删除边的时候最后的顶点有可能是没有出度的,所以这个时候也要使用getOrDefault来避免空指针异常
④ 解决了上面的问题之后那么剩下来的就比较简单了,根据上面写的拓扑排序的步骤即可完成:在队列中删除弹出来的入度为0的顶点,删除与之关联的边,也就是减去关联顶点的入度即可,并将入度为0的顶点加入到队列中一直到队列为空
3. 代码如下:
class Solution {
public static boolean canFinish(int numCourses, int[][] prerequisites) {
Queue<Integer> zeroDegree = new LinkedList<>();
/*使用一个数组来统计入度*/
int indegrees[] = new int[numCourses];
Map<Integer, List<Integer>> map = new HashMap<>();
for (int cur[] : prerequisites){
/*这个二维数组是固定每一维只有两个元素可以看成为出度与入度, 遍历的变量表示每一行*/
indegrees[cur[1]]++;
/*需要注意这里需要先将元素放入到map中再添加元素*/
map.put(cur[0], map.getOrDefault(cur[0], new ArrayList<>()));
map.get(cur[0]).add(cur[1]);
}
/*加入起始条件一开始的入度为零的顶点*/
for (int i = 0; i < numCourses; ++i){
if (indegrees[i] == 0) zeroDegree.add(i);
}
/*统计节点的数量*/
int count = 0;
while (!zeroDegree.isEmpty()){
int curVertex = zeroDegree.poll();
++count;
/*需要注意下面容易发生空指针异常因为有可能当前的顶点是没有边的*/
List<Integer> list = map.getOrDefault(curVertex, new ArrayList<>());
for (int i = 0; i < list.size(); ++i){
int v = list.get(i);
indegrees[v]--;
if (indegrees[v] == 0) zeroDegree.add(v);
}
}
return count == numCourses;
}
}
除了使用map来进行映射在领扣中发现有一个代码写的挺好的,很值得学习一下:
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses];
List<List<Integer>> adjacency = new ArrayList<>();
Queue<Integer> queue = new LinkedList<>();
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
for(int[] cp : prerequisites) {
indegrees[cp[0]]++;
adjacency.get(cp[1]).add(cp[0]);
}
for(int i = 0; i < numCourses; i++)
if(indegrees[i] == 0) queue.add(i);
while(!queue.isEmpty()) {
int pre = queue.poll();
numCourses--;
for(int cur : adjacency.get(pre))
if(--indegrees[cur] == 0) queue.add(cur);
}
return numCourses == 0;
}
}