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

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

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

下期见!

相关推荐
Xの哲學1 分钟前
Linux电源管理深度剖析
linux·服务器·算法·架构·边缘计算
小飞Coding5 分钟前
一文讲透 TF-IDF:如何用一个向量“代表”一篇文章?
算法
算家计算24 分钟前
突然发布!GPT-5.2深夜来袭,3个版本碾压人类专家,打工人该怎么选?
算法·openai·ai编程
s09071361 小时前
Xilinx FPGA 中ADC 数据下变频+ CIC 滤波
算法·fpga开发·fpga·zynq
TL滕2 小时前
从0开始学算法——第十二天(KMP算法练习)
笔记·学习·算法
Math_teacher_fan2 小时前
第二篇:核心几何工具类详解
人工智能·算法
汉克老师2 小时前
CCF-NOI2025第二试题目与解析(第二题、集合(set))
c++·算法·noi·子集卷积·sos dp·mod 异常
mit6.8243 小时前
presum|
算法
不穿格子的程序员3 小时前
从零开始写算法——链表篇2:从“回文”到“环形”——链表双指针技巧的深度解析
数据结构·算法·链表·回文链表·环形链表