一、题目
你这个学期必须选修 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 <= 105
- 0 <= prerequisites.length <= 5000
- prerequisites[i].length == 2
- 0 <= ai, bi < numCourses
- prerequisites[i] 中的所有课程对 互不相同
二、代码
class Solution {
// 课程类
public class Course {
// 该课程在拓扑序列中的入度
public int in;
// 该课程编号
public int courseId;
// 该课程在拓扑序列中的下一个课程集合
public List<Course> nexts;
public Course(int courseId) {
this.in = 0;
this.courseId = courseId;
nexts = new ArrayList<Course>();
}
}
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 过滤无效参数
if (prerequisites == null || prerequisites.length == 0) {
return true;
}
// 构建课程编号和其对应的Course对象的关联表
HashMap<Integer, Course> map = new HashMap<>();
// 遍历prerequisites数组,构造拓扑序列的图
for (int[] a : prerequisites) {
// 要想学课程a[0],就要先学课程a[1],所以在拓扑序列中a[1]在a[0]前面
// 源节点
int from = a[1];
// 目的节点
int to = a[0];
// 如果from课程还没有为其创建Course对象,就创建并加入到map中
if (!map.containsKey(from)) {
map.put(from, new Course(from));
}
// 如果to课程还没有为其创建Course对象,就创建并加入到map中
if (!map.containsKey(to)) {
map.put(to, new Course(to));
}
// 获取from和to课程对象
Course f = map.get(from);
Course t = map.get(to);
// 将t的入度加1
t.in++;
// 将t加入到f的nexts集合中
f.nexts.add(t);
}
// 至此,构造完了拓扑序列的结构
// 入度为0的队列,用来做拓扑序列的遍历
// 只有入度为0的课程,才能保证这个课程所需要的所有前置课程都学完了,才可以遍历到这个入度为0的课程上
// 我们只能遍历从队列中弹出的入读为0的课程节点,这样才能保证遍历到的课程是可以进行学习的
Queue<Course> zeroInQueue = new LinkedList<>();
// 找到拓扑序列中入度为0的节点,将其加入到队列中,这个节点就是拓扑序列的起点
for (Course c : map.values()) {
if (c.in == 0) {
zeroInQueue.add(c);
}
}
// 记录在拓扑序列遍历了多少个节点了,即已经学完了多少个课程了
int cnt = 0;
// 开始遍历拓扑序列
while (!zeroInQueue.isEmpty()) {
// 弹出队列头部,cur为当前遍历到的节点
Course cur = zeroInQueue.poll();
// cur的入度为0,说明他需要的前置课程都已经学完了,所以可以学cur这门课了
// 将已经学完的课程数加1
cnt++;
// 遍历cur课程所有的nexts集合中的课程,cur作为这些课程所需要的前置课程
for (Course nextCourse : cur.nexts) {
// 将每一个nextCourse的入度减1,表示学完cur后,nextCourse所需要的前置课程数少了一个
// 如果nextCourse课程的入度减1之后为0了,说明nextCourse这个可能所需要的前置课程也都学完了,将其加入到队列中,用于后续对其遍历
if (--nextCourse.in == 0) {
zeroInQueue.add(nextCourse);
}
}
}
// map.size()表示全部的课程数
// 如果最多可以学完的课程数cnt 等于 map.size(),就说明我们学完了所有课程,返回true
return cnt == map.size();
}
}
三、解题思路
这道题的本质就是问你这个工作量能不能从一开始的某个工作一直做下而不会出现环,能够一直做到结束,按照顺序把每一件事都做完。
所以这就是一道拓扑排序的题目。