图论专题(十七):从“判定”到“构造”——生成一份完美的「课程表 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. 出队即"排序",并在邻居中减入度。

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

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

下期见!

相关推荐
qq_433554541 小时前
C++ 稀疏表
开发语言·c++·算法
小白程序员成长日记2 小时前
2025.11.21 力扣每日一题
算法·leetcode·职场和发展
小年糕是糕手3 小时前
【C++】C++入门 -- inline、nullptr
linux·开发语言·jvm·数据结构·c++·算法·排序算法
高洁013 小时前
具身智能-普通LLM智能体与具身智能:从语言理解到自主行动
人工智能·深度学习·算法·aigc·知识图谱
星期天23 小时前
3.2联合体和枚举enum,还有动态内存malloc,free,calloc,realloc
c语言·开发语言·算法·联合体·动态内存·初学者入门·枚举enum
Andy4 小时前
回文子串数目--动态规划算法
算法·动态规划
sin_hielo4 小时前
leetcode 1930
算法·leetcode
塞北山巅4 小时前
相机自动曝光(AE)核心算法——从参数调节到亮度标定
数码相机·算法
聆风吟º4 小时前
【数据结构入门手札】算法核心概念与复杂度入门
数据结构·算法·复杂度·算法的特性·算法设计要求·事后统计方法·事前分析估算方法