问题简介
题解github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions
题目描述
你这个学期必须选修 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 <= 20000 <= prerequisites.length <= 5000prerequisites[i].length == 20 <= ai, bi < numCoursesai != bi- 所有
[ai, bi]互不相同
解题思路
本题本质上是判断有向图中是否存在环。如果存在环,则无法完成所有课程(因为会出现循环依赖);否则可以完成。
我们可以将课程看作图中的节点,先修关系看作有向边(bi → ai 表示 bi 是 ai 的先修课)。问题转化为:判断该有向图是否为有向无环图(DAG)。
方法一:拓扑排序(Kahn 算法)
💡 核心思想:利用入度进行 BFS 拓扑排序。
- 构建邻接表和入度数组。
- 将所有入度为 0 的课程加入队列。
- 依次从队列中取出课程,将其指向的课程入度减 1;若某课程入度变为 0,则加入队列。
- 最终若处理的课程数等于
numCourses,说明无环,返回true;否则存在环,返回false。
✅ 优点:直观、易于理解,同时可输出拓扑序列。
方法二:DFS + 三色标记法
💡 核心思想:通过 DFS 遍历检测回边(back edge)。
使用三种状态标记节点:
- 0(未访问):尚未访问该节点。
- 1(正在访问):当前 DFS 路径中正在访问该节点(即在递归栈中)。
- 2(已完成):该节点及其所有后继已访问完毕,无环。
若在 DFS 过程中遇到状态为 1 的节点,说明存在环。
✅ 优点:空间效率略高,适合稀疏图。
代码实现
java
// Java 实现
// 方法一:拓扑排序(Kahn 算法)
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 构建邻接表和入度数组
List<List<Integer>> graph = new ArrayList<>();
int[] indegree = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
for (int[] pre : prerequisites) {
int from = pre[1], to = pre[0];
graph.get(from).add(to);
indegree[to]++;
}
// BFS
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (indegree[i] == 0) {
queue.offer(i);
}
}
int visited = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
visited++;
for (int next : graph.get(course)) {
indegree[next]--;
if (indegree[next] == 0) {
queue.offer(next);
}
}
}
return visited == numCourses;
}
}
// 方法二:DFS + 三色标记
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
for (int[] pre : prerequisites) {
graph.get(pre[1]).add(pre[0]);
}
int[] color = new int[numCourses]; // 0: unvisited, 1: visiting, 2: visited
for (int i = 0; i < numCourses; i++) {
if (color[i] == 0 && hasCycle(graph, color, i)) {
return false;
}
}
return true;
}
private boolean hasCycle(List<List<Integer>> graph, int[] color, int node) {
if (color[node] == 1) return true; // 发现环
if (color[node] == 2) return false; // 已完成,无环
color[node] = 1;
for (int neighbor : graph.get(node)) {
if (hasCycle(graph, color, neighbor)) {
return true;
}
}
color[node] = 2;
return false;
}
}
go
// Go 实现
// 方法一:拓扑排序(Kahn 算法)
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make([][]int, numCourses)
indegree := make([]int, numCourses)
for _, pre := range prerequisites {
from, to := pre[1], pre[0]
graph[from] = append(graph[from], to)
indegree[to]++
}
queue := []int{}
for i := 0; i < numCourses; i++ {
if indegree[i] == 0 {
queue = append(queue, i)
}
}
visited := 0
for len(queue) > 0 {
course := queue[0]
queue = queue[1:]
visited++
for _, next := range graph[course] {
indegree[next]--
if indegree[next] == 0 {
queue = append(queue, next)
}
}
}
return visited == numCourses
}
// 方法二:DFS + 三色标记
func canFinish(numCourses int, prerequisites [][]int) bool {
graph := make([][]int, numCourses)
for _, pre := range prerequisites {
graph[pre[1]] = append(graph[pre[1]], pre[0])
}
color := make([]int, numCourses) // 0: unvisited, 1: visiting, 2: visited
var hasCycle func(int) bool
hasCycle = func(node int) bool {
if color[node] == 1 {
return true
}
if color[node] == 2 {
return false
}
color[node] = 1
for _, neighbor := range graph[node] {
if hasCycle(neighbor) {
return true
}
}
color[node] = 2
return false
}
for i := 0; i < numCourses; i++ {
if color[i] == 0 && hasCycle(i) {
return false
}
}
return true
}
示例演示
以 numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]] 为例:
课程依赖图:
0 → 1 → 3
↓ ↗
2 ────┘
- 拓扑排序过程 :
- 初始入度:
[0,1,1,2] - 入度为 0 的课程:
0 - 处理
0→1和2入度减为 0 → 加入队列 - 处理
1→3入度减为 1 - 处理
2→3入度减为 0 → 加入队列 - 处理
3 - 共处理 4 门课程 ⇒ 返回
true
- 初始入度:
✅ 无环,可以完成。
答案有效性证明
- 拓扑排序法 :若图中存在环,则环上所有节点入度 ≥1,永远不会被加入队列,最终
visited < numCourses。 - DFS 三色法:若存在环,则在 DFS 路径中必会再次访问到状态为"正在访问"的节点(即回边),从而检测到环。
两种方法均能充要地判断有向图是否存在环,因此解法正确。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 拓扑排序(Kahn) | O(V + E) | O(V + E) |
| DFS 三色标记 | O(V + E) | O(V + E) |
其中:
- V =
numCourses(顶点数) - E =
prerequisites.length(边数)
📌 两种方法时间/空间复杂度相同,实际性能取决于图的结构和语言运行时。
问题总结
✅ 关键洞察:课程安排问题 ⇨ 有向图环检测 ⇨ 拓扑排序 / DFS 环检测。
✅ 适用场景:
- 拓扑排序:需输出合法顺序时优先使用。
- DFS 三色法:仅需判断可行性时代码更简洁。
❌ 常见误区:
- 忽略图可能不连通(需遍历所有节点)。
- 使用普通 visited 标记(无法区分"正在访问"和"已完成")。
💡 扩展思考:
- 若需输出任意一个合法课程顺序?→ 拓扑排序结果即为答案。
- 若课程有学分、学期限制?→ 可结合动态规划或分层拓扑排序。
本题是图论基础应用的经典代表,掌握其解法对解决依赖关系类问题至关重要。