【板子】拓扑排序

一、核心原理

1. 它是干什么的?

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

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

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

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 < 5000vector<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
相关推荐
码匠许师傅10 小时前
【C++ 面试真题】C++ 的 const 和 constexpr 有什么区别?
c++·面试
坚持编程的菜鸟11 小时前
模拟实现memmove
c语言·算法·模拟实现memmove
wabs66613 小时前
关于图论【最短路径之Dijkstra算法(堆优化版)|卡码网47.参加科学大会的思考】
数据结构·算法·图论·优先级队列·邻接表·小顶堆·卡码网
果果燕13 小时前
实习笔记(一):NFS、Samba、VNC、Git、CMake、systemd、内核、交叉编译
linux·arm开发·c++·笔记·git
坚持编程的菜鸟13 小时前
编写判断大小端程序
c语言·算法·判断大小端
papaofdoudou13 小时前
判断排列逆序奇偶性的乘积判别法(范德蒙德符号法)
人工智能·算法
牛艺翔13 小时前
C++基础
开发语言·c++
键盘会跳舞13 小时前
C++ :容器适配器stack源码级拆解
开发语言·c++··stack·先进后出
linux-hzh13 小时前
百日算法修炼 · Day 03
java·算法
问商十三载14 小时前
RAG 检索效果差怎么排查?2026 五层诊断法完整指南
开发语言·人工智能·windows·python·算法