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

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

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

下期见!

相关推荐
Nebula_g21 小时前
线程进阶: 无人机自动防空平台开发教程(更新)
java·开发语言·数据结构·学习·算法·无人机
rit843249921 小时前
基于MATLAB的环境障碍模型构建与蚁群算法路径规划实现
开发语言·算法·matlab
hoiii18721 小时前
MATLAB SGM(半全局匹配)算法实现
前端·算法·matlab
独自破碎E21 小时前
大整数哈希
算法·哈希算法
纤纡.21 小时前
逻辑回归实战进阶:交叉验证与采样技术破解数据痛点(二)
算法·机器学习·逻辑回归
czhc114007566321 小时前
协议 25
java·开发语言·算法
范纹杉想快点毕业21 小时前
状态机设计与嵌入式系统开发完整指南从面向过程到面向对象,从理论到实践的全面解析
linux·服务器·数据库·c++·算法·mongodb·mfc
fish-man1 天前
测试加粗效果
算法
deep_drink1 天前
【基础知识一】线性代数的核心:从矩阵变换到 SVD 终极奥义
线性代数·机器学习·矩阵
晓13131 天前
第二章 【C语言篇:入门】 C 语言基础入门
c语言·算法