图论专题(十七):从“判定”到“构造”——生成一份完美的「课程表 II」

哈喽各位,我是前端小L。

欢迎来到我们的图论专题第十七篇!上一题我们解决了"能不能修完"的问题(Yes/No),今天我们要解决"怎么修"的问题(输出路径)。

这其实是算法中一种常见的进阶模式:存在性判定 -> 构造性求解 。好消息是,对于拓扑排序而言,只要我们理解了上一题的 Kahn 算法,这道题几乎就是"送分题"。因为 Kahn 算法的执行过程,本身就是在按顺序"剥离"课程,这个剥离的顺序,天然就是我们想要的答案!

力扣 210. 课程表 II

https://leetcode.cn/problems/course-schedule-ii/

题目分析:

  • 输入 :课程数 numCourses,依赖关系 prerequisites

  • 目标 :返回任意一个合法的修课顺序。

  • 特殊情况 :如果无法完成所有课程(有环),返回空数组 []

核心洞察:Kahn 算法的"自然顺序"

让我们回顾一下 Kahn 算法的核心逻辑:

总是寻找当前入度为 0(没有任何前置依赖,或者前置依赖都已修完)的节点,修完它,然后把它指向的后续节点的入度减 1。

这不就是我们修课的自然逻辑吗?

  1. 我看一眼课表,发现 A 课不需要先修课。好,A 放入我的课表

  2. 修完 A 后,原本需要先修 A 才能学的 B 课,现在变成了"无门槛"状态。

  3. 好,B 放入我的课表

  4. ...

结论: 在 Kahn 算法中,节点从队列 q 中"出队"的顺序 ,或者说我们把节点加入队列的顺序,就是一个合法的拓扑排序结果!

我们只需要在上一题的代码基础上,增加一个 vector<int> result,每当一个节点出队时,把它 push_back 进去即可。

算法流程 (BFS - Kahn)

  1. 建图 & 统计入度

    • 邻接表 adj,入度数组 indegree
  2. 初始化队列

    • 将所有初始 indegree[i] == 0 的课程入队。
  3. BFS 生成顺序

    • while (!q.empty())

      • curr = q.front(); q.pop();

      • 关键一步result.push_back(curr); (记录修课顺序)

      • 遍历 curr 的邻居 next

        • indegree[next]--

        • if (indegree[next] == 0)q.push(next)

  4. 最终检查

    • 如果 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 算法,还学会了如何利用它来构造解。 这道题是拓扑排序最直接的应用。记住这个模式:

  1. 统计入度

  2. 入度为0进队列

  3. 出队即"排序",并在邻居中减入度。

这套"三板斧",能解决绝大多数依赖排序问题。

在下一篇中,我们将把拓扑排序的思想应用到一个更有趣的场景:"安全节点" 。我们要寻找那些无论怎么走,最终都能停下来的安全点。这需要我们稍微转换一下思维------逆向拓扑排序

下期见!

相关推荐
吃好睡好便好5 小时前
在Matlab中绘制横直方图
开发语言·学习·算法·matlab
仰泳之鹅5 小时前
【C语言】自定义数据类型2——联合体与枚举
c语言·开发语言·算法
2601_957787586 小时前
矩阵运营的技术底座:为什么“一体化系统“正在取代“工具拼装“
人工智能·矩阵·矩阵运营
x_yeyue7 小时前
三角形数
笔记·算法·数论·组合数学
念何架构之路9 小时前
Go语言加密算法
数据结构·算法·哈希算法
AI科技星9 小时前
《数学公理体系·第三部·数术几何》(2026 年版)
c语言·开发语言·线性代数·算法·矩阵·量子计算·agi
失去的青春---夕阳下的奔跑9 小时前
560. 和为 K 的子数组
数据结构·算法·leetcode
黎阳之光9 小时前
黎阳之光:以视频孪生重构智慧医院信息化,打造高标项目核心竞争力
大数据·人工智能·物联网·算法·数字孪生
丷丩10 小时前
三级缓存下MVT地图瓦片服务性能优化策略
算法·缓存·性能优化·gis·geoai-up
m0_6294947310 小时前
LeetCode 热题 100-----25.回文链表
数据结构·算法·leetcode·链表