前言
题目:
leetcode-----课程表
题目描述:
你这个学期必须选修 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 。这是不可能的。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/course-schedule
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
一、解题思路
通过题目描述,不难想到
1.我们先需要找到不需要依赖其他课程的课(如没有直接返回false,否则进行下一步)
2.这些不需要依赖其他课程的课,都放到一个队列里面,然后把他们一个个出队,再判断,学完这个课之后我们就能学哪些课了,这时候就能判断出,这个题应该是考你宽度优先搜索算法
3.存在依赖关系还是宽度优先搜索,这不就是拓扑排序吗,到这一步我们确定了要使用的算法
4.确定该使用什么算法后,就可以考虑大体流程了
5.假设[1,2],表示1依赖于2,也就是要学1就要先学2,表示为1<——2,2指向1,1的入度为1,2的入度为0,反过来也一样,不过就是找出度为0的点,定义好规则之后,就可以按拓扑排序的思路来了
6.大体步骤如下:
a) 先遍历一遍prerequisites数组,把初始情况下每一个节点的入度情况标记一下,同时,每个节点于其他节点的联系也要放到一个Map<Integer, List< Integer >>中存起来
b)找出入度为0的节点,把他放到队列中
c)然后循环,每取出一个节点的时候,根据map找到依赖于这个节点的各个点,然后在记录入度的数组中将这个点的入度减一,如果这个过程中某个点的入度为0了,就把这个点加入到队列中
d)循环结束,如果每个点都进入过队列,证明可以完成课程,如果有没有的,就不能,在上一步可以用一个set记录一下进入过队列的点,最后比较长度就好了
二、代码
a:
Set<Integer> studied = new LinkedHashSet<>();
int[] inDegree = new int[numCourses]; //记录各节点的入度
Map<Integer, List<Integer>> relMap = new HashMap<>(); //记录依赖情况
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses ; i++) {
relMap.put(i,new LinkedList<>()); //因为依赖并不是一个,而是列表,所以要先创建好列表
}
for (int i = 0; i < prerequisites.length; i++) {
int node = prerequisites[i][0];
inDegree[node] ++;
List<Integer> list = relMap.get(prerequisites[i][1]);
list.add(prerequisites[i][0]);
relMap.put(prerequisites[i][1],list);
}//遍历关系数组,把入度和依赖关系补充完成
b:找出入度为0的节点,把他放到队列中
for (int i = 0; i < inDegree.length; i++) {
if (inDegree[i] == 0){
queue.offer(i);
studied.add(i);
}
}
c:循环
while (!queue.isEmpty()){
Integer poll = queue.poll();
List<Integer> list = relMap.get(poll);
for (Integer node : list) {
inDegree[node]--;
}
for (int i = 0; i < inDegree.length; i++) {
if (inDegree[i] == 0 && !studied.contains(i)){
queue.offer(i);
studied.add(i);
}//找出入度为0的点,把他放入队列
}
}
d:返回结果
return studied.size() == numCourses;
三、注意事项
在实现这个思路的时候,我出现过很多错误。这里记录一下。
错误一:
c步骤中,我之前使用的代码是这样的:
while (!queue.isEmpty()){
Integer poll = queue.poll();
studied.add(poll);
List<Integer> list = relMap.get(poll);
for (Integer node : list) {
inDegree[node]--;
}
for (int i = 0; i < inDegree.length; i++) {
if (inDegree[i] == 0 && !studied.contains(i)){
queue.offer(i);
}
}
}
这样写带来了一个错误,很多节点会多次重复进入队列
所以进行了改进,不再是出队的时候把节点放入set,而是在入队的时候把节点放入set
然而,这里还有一个不足,就是我每次都要遍历所有节点,看他们是不是入度为0,这样其实大大增加了时间复杂度,因此改为判断入度改变后的节点入度是不是变为0了,如果是,直接加入队列
四、最终版本
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
Set<Integer> studied = new LinkedHashSet<>();
int[] inDegree = new int[numCourses];
Map<Integer, List<Integer>> relMap = new HashMap<>();
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses ; i++) {
relMap.put(i,new LinkedList<>());
}
for (int i = 0; i < prerequisites.length; i++) {
int node = prerequisites[i][0];
inDegree[node] ++;
List<Integer> list = relMap.get(prerequisites[i][1]);
list.add(prerequisites[i][0]);
relMap.put(prerequisites[i][1],list);
}
for (int i = 0; i < inDegree.length; i++) {
if (inDegree[i] == 0){
queue.offer(i);
studied.add(i); //改进1
}
}
while (!queue.isEmpty()){
Integer poll = queue.poll();
studied.add(poll); //改进2:这一行去掉
List<Integer> list = relMap.get(poll);
for (Integer node : list) {
inDegree[node]--;
if (inDegree[node] == 0 && !studied.contains(node)){
queue.offer(node);
studied.add(node); //改进3
}
}
}
return studied.size() == numCourses;
}
}