【板子】拓扑排序

一、核心原理

1. 它是干什么的?

解决 **"先做 A 才能做 B"**​ 的问题。

  • 把 **有向无环图 (DAG)**​ 拉直成一个序列。

  • 保证:如果 A -> B(A 依赖 B),序列中 B 一定在 A 前面 (注:取决于建图逻辑,通常我们定义 a->b为 a做完才能做 b,即 a在前;但在"菜肴制作"题中,为了"小数靠前",我们采用了反向建图,此时 y->x意味着 x依赖 y,y在前)。

2. 为什么要有"无环"?

如果有环 A->B->C->A:

  • 意味着 A 要在 B 前,B 要在 C 前,C 要在 A 前。

  • 逻辑死锁,无解。

  • 判环依据 :拓扑排序输出的节点数 < n。

3. Kahn 算法(BFS)底层逻辑

  • **入度 (in)**​ = 有多少人在等你。

  • 入度为 0​ = 没人等你,你现在就可以做。

  • 做完一个任务,就告诉你的后继:"我可以了"(入度减 1)。

  • 当某个后继发现"所有人都搞定了"(入度变 0),把它加入队列。


二、通用算法板子(Kahn / BFS)

这是最常用、最安全的板子,适用于 90% 的题。

1. 链式前向星存图(推荐)

复制代码
const int N = 1e5 + 10, M = 2e5 + 10;
int h[N], e[M], ne[M], idx;
int in[N]; // 入度

void add(int a, int b) { // a->b
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

// 初始化
memset(h, -1, sizeof h);
idx = 0;
fill(in, in + n + 1, 0);

2. 拓扑排序主逻辑

复制代码
bool topo(vector<int>& res) {
    queue<int> q;
    // 1. 把所有入度为0的点入队
    for (int i = 1; i <= n; i++) {
        if (in[i] == 0) q.push(i);
    }

    // 2. BFS
    while (q.size()) {
        int u = q.front(); q.pop();
        res.push_back(u);
        // 3. 遍历邻接表,减少后继的入度
        for (int i = h[u]; ~i; i = ne[i]) {
            int v = e[i];
            if (--in[v] == 0) {
                q.push(v);
            }
        }
    }
    // 4. 判环:如果排出的点不够,说明有环
    return res.size() == n;
}

三、板子的三种"变身"(应对不同题型)

普通情况(随便排,只要合法)

  • 特征:题目只要求给出一种可行的顺序。

  • 板子 :直接用上面的 queue<int>。

字典序最小

  • 特征:输出时,如果多个点可选,选编号小的。

  • 变身 :把 queue换成 priority_queue<less<int>>(小根堆)。

    priority_queue<int, vector, greater> q;

  • 注意 :这是正向思维,谁小谁先出。

特殊限制:小数尽量靠前(洛谷 P3243 菜肴制作)

  • 特征:1 要尽量靠前,在保证 1 的前提下 2 要尽量靠前......

  • 误区:不能用小根堆直接跑。

  • 正解 :反向建图 + 大根堆 + 反转结果。

    // 建图时:add(y, x); 原来是 x 依赖 y,现在建 y->x
    // 队列:priority_queue q; // 大根堆
    // 输出:reverse(res.begin(), res.end());

  • 原理 :与其纠结谁在前,不如让大数在后面排队。


四、DFS 拓扑排序

用于判环 或内存受限场景。

复制代码
int st[N]; // 0=未访问, 1=访问中, 2=已结束
vector<int> res;

bool dfs(int u) {
    st[u] = 1; // 进入递归栈
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (st[v] == 1) return false; // 遇到环
        if (st[v] == 0 && !dfs(v)) return false;
    }
    st[u] = 2; // 弹出递归栈
    res.push_back(u); // 后序插入
    return true;
}

bool topo_dfs() {
    for (int i = 1; i <= n; i++) {
        if (!st[i] && !dfs(i)) return false;
    }
    reverse(res.begin(), res.end()); // 必须反转
    return true;
}

五、临场使用决策树

考试时,按这个顺序想:

  1. 是不是 DAG?

    • 是 → 拓扑排序

    • 否 → 强连通分量(Tarjan)

  2. 有没有环?

    • 有 → 输出 Impossible

    • 无 → 继续

  3. 有没有特殊顺序要求?

    • 无 → queue

    • 字典序最小 → priority_queue<greater<int>>

    • 小数尽量前 → 反向建图 + priority_queue<less<int>>+ reverse

  4. 数据多大?

    • n > 5000→ 链式前向星

    • n < 5000→ vector<vector<int>>


六、踩坑点

坑点 症状 解决方案
多测不清空​ WA / TLE h, idx, in必须重置
数组开小​ RE N至少开 max(n,m)+10
下标混淆​ WA 看清题目是 0~n-1还是 1~n
判环条件​ WA res.size() != n
DFS 忘反转​ WA DFS 拓扑必须 reverse
相关推荐
Shimeri5 分钟前
越卷越像:我用遗传算法做了个"内卷"实验
算法·three.js·数据可视化·遗传算法·进化算法
解决小子9 分钟前
【虚幻引擎】【C++】【GAS框架】构建角色基类、属性集、数值钳制
c++·笔记·游戏引擎·虚幻·gas·能力系统
Evand J28 分钟前
【PMSM例程1】永磁同步电机(PMSM)转速与转子电角度观测算法|基于扩展卡尔曼滤波(EKF)的传感器状态估计与误差。附下载链接
算法·电机·1024程序员节·卡尔曼滤波
guo_wen_qiang29 分钟前
二叉树的层序遍历-使用双端队列Deque
算法·leetcode·1024程序员节
爱读书的小胖1 小时前
LeetCode 55 跳跃游戏 C 语言解题分析
c语言·数据结构·算法·leetcode·1024程序员节
CCblandy1 小时前
前缀缓存命中率不靠引擎靠排列:本机仿真 3 轮,易变字段放开头 98.0%→0.0%
人工智能·python·算法·自然语言处理·性能优化
上班摸鱼下班游戏1 小时前
Unity 仿生智能算法 从蚁群算法 开始
算法·unity·游戏引擎
pipixia200331 小时前
C++ 面试高频:namespace 的 6 个知识点,从 using 到匿名命名空间
c语言·学习·算法·嵌入式·命名空间·c++面试·匿名命名空间
lunar_coder1 小时前
【C++】如何解决智能指针的析构函数,如何实现shared_ptr的引用计数以及解决shared_ptr的循环引用
c++·1024程序员节
白白白小纯2 小时前
C++进阶.多态
c++