【板子】拓扑排序

一、核心原理

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
相关推荐
某不知名網友2 小时前
项目:轻量级搜索引擎
c++
算法备案代理2 小时前
宽带自动连接保姆级教程
算法
罗超驿2 小时前
双指针算法详解:从入门到精通(Java版)
算法·leetcode·职场和发展
m0_377062922 小时前
电池电量计库伦算法计算公式
算法
patrickpdx2 小时前
高联预赛中的高斯函数问题
算法
tkevinjd2 小时前
力扣300-最长递增子序列
算法·leetcode·职场和发展·动态规划·贪心
A_humble_scholar3 小时前
Linux(十七)深入多线程编程:同步原语与实战指南
linux·运维·c++
小李飞刀李寻欢4 小时前
DeepSeek V3 版本模型结构分析
算法·大模型·deepseek
某不知名網友4 小时前
C++ 七大排序算法完整讲解
java·算法·排序算法