一、核心原理
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_queueq; // 大根堆
// 输出: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;
}
五、临场使用决策树
考试时,按这个顺序想:
-
是不是 DAG?
-
是 → 拓扑排序
-
否 → 强连通分量(Tarjan)
-
-
有没有环?
-
有 → 输出
Impossible -
无 → 继续
-
-
有没有特殊顺序要求?
-
无 →
queue -
字典序最小 →
priority_queue<greater<int>> -
小数尽量前 → 反向建图 +
priority_queue<less<int>>+reverse
-
-
数据多大?
-
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 |