题目

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
数据范围
1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i]中的所有课程对 互不相同
测试用例
示例1
java
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例2
java
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
题解
java
import java.util.Arrays;
class Solution {
// idx 用于给每一条边分配唯一的索引(相当于链表节点的内存地址)
int idx = 0;
// N 是数组容量。注意:这里包含了节点头数组 h 和边数组 e, ne。
// 警告:如果题目中 prerequisites 的长度(边数)超过 5000,e 和 ne 数组会越界。
// 建议:e 和 ne 的大小最好根据 prerequisites.length 来初始化,或者开得比 N 大很多。
int N = 5001;
// h[u] 存储节点 u 的第一条边的索引(Head),初始化为 -1 表示没有边
int h[] = new int[N];
// e[i] 存储第 i 条边指向的目标节点(Edge/End),相当于链表节点的 value
int e[] = new int[N];
// ne[i] 存储第 i 条边的下一条同起点边的索引(Next Edge),相当于链表节点的 next 指针
int ne[] = new int[N];
// valid 标志位,一旦发现环,设为 false
boolean valid = true;
// visited 数组用于三色标记法:
// 0: 未访问 (Unvisited)
// 1: 正在访问 (Visiting) - 当前递归栈中,如果再次遇到说明有环
// 2: 已完成 (Visited) - 该节点及其子节点都已检查无环
int visited[];
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 初始化头节点数组为 -1
Arrays.fill(h, -1);
visited = new int[numCourses];
// 建图:遍历先修课程数组
// info[0] 是课程,info[1] 是先修课。也就是 info[1] -> info[0]
for(int[] info : prerequisites){
add(info[1], info[0]);
}
// 遍历所有课程,处理非连通图的情况
// 只要 valid 为 true 且还有未访问节点,就继续 DFS
for(int i = 0; i < numCourses && valid; i++){
if(visited[i] == 0){
dfs(i);
}
}
return valid;
}
public void dfs(int u){
// 标记当前节点 u 为"正在访问"(入栈)
visited[u] = 1;
// 链式前向星遍历:
// i = h[u]: 获取 u 的第一条边的索引
// i != -1: 还有边没遍历完
// i = ne[i]: 跳到下一条边的索引
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i]; // 【关键】取出当前边 i 指向的邻居节点 j
if(visited[j] == 0){
// 如果邻居 j 没访问过,递归访问
dfs(j);
// 如果递归回来发现已经有环了,直接返回,剪枝
if(!valid){
return;
}
} else if(visited[j] == 1){
// 如果邻居 j 状态是 1,说明 j 还在当前的递归栈中
// 这意味着我们从 u 指回了它的祖先 j,发现了环!
valid = false;
return;
}
// 如果 visited[j] == 2,说明该节点安全,直接跳过
}
// 当前节点 u 的所有邻居都遍历完了,且没有发现环
// 标记为"已完成"(出栈)
visited[u] = 2;
}
// 添加边的函数(头插法)
// a -> b (a 指向 b)
public void add(int a, int b){
e[idx] = b; // 1. 记录第 idx 条边的终点是 b
ne[idx] = h[a]; // 2. 第 idx 条边的 next 指向节点 a 原来的第一条边
h[a] = idx++; // 3. 更新节点 a 的第一条边为当前边 idx,并将 idx 加 1
}
}
思路
这道题难点在于知道他想考什么,什么情况下选课会失败,就是当课程的前置要求之间形成了一个环。对于前置要求,我们使用拓扑排序可以满足其性质,我们在实现拓扑排序的图中,记录并判断是否存在环即可。存在无环拓扑排序即满足题目要求返回true,反之