【板子】拓扑排序

一、核心原理

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
相关推荐
大侠归来4 分钟前
C 语言 | 在函数中操作数组
c语言·c++·算法
怕浪猫5 小时前
GEO 优化到底是什么?AI时代内容创作者必须懂的新技能
算法·面试·github
朝朝辞暮i6 小时前
C++ 第 23 课:class —— 开始真正进入面向对象
开发语言·c++·算法
Evand J7 小时前
【MATLAB例程】三维RRT+APF避障路径规划与到达角(AOA)定位算法|三维路径优化与定位仿真例程
算法·matlab·路径规划·代码·定位·rrt·aoa
郑同学的笔记7 小时前
【c++随笔28】strcmp 和 memcmp对比
开发语言·c++
随意起个昵称8 小时前
【背包dp】输出方案路径
c++·动态规划
weixin_307779138 小时前
有限产能智能排产与动态重排智能体:从需求解构到技术实现
开发语言·人工智能·算法·架构
朝朝辞暮i8 小时前
C++ 第 27 课:智能指针 shared_ptr
开发语言·c++·算法
by209999 小时前
学会使用std::string类,并理解其内部是如何管理字符串的详细阐述(上)
c++·笔记·字符串·类和对象·string
沙漠之主9 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++