哈喽各位,我是前端小L。
欢迎来到我们的图论专题第十七篇!上一题我们解决了"能不能修完"的问题(Yes/No),今天我们要解决"怎么修"的问题(输出路径)。
这其实是算法中一种常见的进阶模式:存在性判定 -> 构造性求解 。好消息是,对于拓扑排序而言,只要我们理解了上一题的 Kahn 算法,这道题几乎就是"送分题"。因为 Kahn 算法的执行过程,本身就是在按顺序"剥离"课程,这个剥离的顺序,天然就是我们想要的答案!
力扣 210. 课程表 II
https://leetcode.cn/problems/course-schedule-ii/

题目分析:
-
输入 :课程数
numCourses,依赖关系prerequisites。 -
目标 :返回任意一个合法的修课顺序。
-
特殊情况 :如果无法完成所有课程(有环),返回空数组
[]。
核心洞察:Kahn 算法的"自然顺序"
让我们回顾一下 Kahn 算法的核心逻辑:
总是寻找当前入度为 0(没有任何前置依赖,或者前置依赖都已修完)的节点,修完它,然后把它指向的后续节点的入度减 1。
这不就是我们修课的自然逻辑吗?
-
我看一眼课表,发现
A课不需要先修课。好,把A放入我的课表。 -
修完
A后,原本需要先修A才能学的B课,现在变成了"无门槛"状态。 -
好,把
B放入我的课表。 -
...
结论: 在 Kahn 算法中,节点从队列 q 中"出队"的顺序 ,或者说我们把节点加入队列的顺序,就是一个合法的拓扑排序结果!
我们只需要在上一题的代码基础上,增加一个 vector<int> result,每当一个节点出队时,把它 push_back 进去即可。
算法流程 (BFS - Kahn)
-
建图 & 统计入度:
- 邻接表
adj,入度数组indegree。
- 邻接表
-
初始化队列:
- 将所有初始
indegree[i] == 0的课程入队。
- 将所有初始
-
BFS 生成顺序:
-
while (!q.empty()):-
curr = q.front(); q.pop(); -
关键一步 :
result.push_back(curr);(记录修课顺序) -
遍历
curr的邻居next:-
indegree[next]-- -
if (indegree[next] == 0),q.push(next)。
-
-
-
-
最终检查:
-
如果
result.size() == numCourses,说明所有课都修完了,返回result。 -
否则(有环,导致某些课永远入不了队),返回空数组
{}。
-
代码实现
C++
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
// 1. 建图 + 统计入度
vector<vector<int>> adj(numCourses);
vector<int> indegree(numCourses, 0);
for (const auto& relation : prerequisites) {
int course = relation[0];
int prereq = relation[1];
// 边:prereq -> course
adj[prereq].push_back(course);
indegree[course]++;
}
// 2. 将所有入度为 0 的节点入队
queue<int> q;
for (int i = 0; i < numCourses; ++i) {
if (indegree[i] == 0) {
q.push(i);
}
}
// 3. BFS 生成结果
vector<int> result;
while (!q.empty()) {
int curr = q.front();
q.pop();
// 记录当前修完的课
result.push_back(curr);
for (int next : adj[curr]) {
indegree[next]--;
if (indegree[next] == 0) {
q.push(next);
}
}
}
// 4. 检查是否包含所有课程(即无环)
if (result.size() == numCourses) {
return result;
} else {
return {};
}
}
};
进阶思考:DFS 也能做吗?
当然可以!DFS 也可以生成拓扑排序。 其核心思想是:"逆后序" (Reverse Post-order)。
-
当
dfs(u)执行完毕(即将返回,状态变为 2/Black)时,意味着u的所有 后续课程(依赖u的课)都已经先被访问过了(或者将在未来的递归栈中被处理)。 -
这听起来有点绕。其实更简单的理解是:只有当你把
u的所有后路都探完了,确认u是安全的,你才算真正"搞定"了u。 -
我们把"搞定"的节点压入一个栈。
-
最后,栈顶到栈底 的顺序,就是拓扑排序的顺序(或者直接用
vector存储,最后reverse一下)。
(不过,对于这道题,Kahn 算法的逻辑更符合直觉,是首选解法。)
深度复杂度分析
-
V :课程数。E:依赖关系数。
-
时间复杂度 O(V + E):
-
建图 O(E)。
-
每个节点进出队列一次 O(V)。
-
每条边被遍历一次 O(E)。
-
-
空间复杂度 O(V + E):
-
邻接表 O(V + E)。
-
队列和结果数组 O(V)。
-
总结
今天,我们不仅复习了 Kahn 算法,还学会了如何利用它来构造解。 这道题是拓扑排序最直接的应用。记住这个模式:
-
统计入度。
-
入度为0进队列。
-
出队即"排序",并在邻居中减入度。
这套"三板斧",能解决绝大多数依赖排序问题。
在下一篇中,我们将把拓扑排序的思想应用到一个更有趣的场景:"安全节点" 。我们要寻找那些无论怎么走,最终都能停下来的安全点。这需要我们稍微转换一下思维------逆向拓扑排序。
下期见!