来源:算法讲解059【必备】建图、链式前向星、拓扑排序 · 2026-10-25
依据:视频字幕 2000 字
用三种方式(邻接矩阵、邻接表、链式前向星)把图存进计算机,再用拓扑排序(Topological Sort)对有向无环图(DAG)求出一个线性顺序。
vector<vector<int>> 的堆分配和指针跳转太慢、list/map 更不能用,需要一块连续静态数组把边存下来。head/tail 手写,比 std::queue 常数小。三种建图方式的直觉:
g[u][v] 直接查,O(1) 判边,但空间 O(n²),n 上万就爆。head[u](第一条出边的下标),每条边存 next[i](同起点的下一条边的下标)。遍历时 for (int i = head[u]; i != -1; i = nxt[i]),效果和邻接表一样,但全部落在连续内存里,cache 友好、无堆分配。拓扑排序为什么对(Kahn 算法):
DAG 里一定存在入度为 0 的点(否则沿着入边一直往回走必然成环)。把入度为 0 的点拿出来放进答案,然后「删掉」它的所有出边——等价于把它指向的点的入度减 1。减到 0 的点说明它所有前驱都已经排进答案了,可以安全入队。反复做,直到队列空。
直觉:每次取「当前没有任何未完成前置任务」的任务,这是唯一合法的执行顺序来源。
以链式前向星 + Kahn 拓扑排序为例:
head[n] 初始化为 -1;to[m]、nxt[m]、w[m](带权才要);deg[n] 入度清零;cnt = 0 边计数器。add(u, v, w): - to[cnt] = v; w[cnt] = w; nxt[cnt] = head[u]; head[u] = cnt++;
- 若是无向图,再 add(v, u, w) 一次。
- 同时 deg[v]++(有向图只加被指向那端)。
deg[i] == 0 的点入队(手写数组队列)。i = head[u] ... i = nxt[i],对 v = to[i] 做 if (--deg[v] == 0) q[tail++] = v;。有向图,4 个点(1~4),边:1→3, 4→3, 2→4, 1→2, 2→3, 3→1。
先看这个图:1→2→3→1 构成环,所以不是 DAG,拓扑排序会失败。为了演示成功流程,改用边集:1→3, 1→2, 2→4, 4→3(无环)。
初始入度:deg[1]=0, deg[2]=1, deg[3]=2, deg[4]=1。
| 步骤 | 队列 | 出队 | 答案 | 入度变化 |
|---|---|---|---|---|
| 初始 | [1] | - | [] | deg = [0,1,2,1] |
| 1 | [] | 1 | [1] | 1→3: deg[3]=1;1→2: deg[2]=0,入队 |
| 2 | [2] | 2 | [1,2] | 2→4: deg[4]=0,入队 |
| 3 | [4] | 4 | [1,2,4] | 4→3: deg[3]=0,入队 |
| 4 | [3] | 3 | [1,2,4,3] | 无出边 |
答案长度 4 == n,成功。拓扑序 1 2 4 3(不唯一,1 2 3 4 也合法?不合法,因为 4→3 要求 4 在 3 前)。
Python(邻接表 + Kahn,好读版):
from collections import deque
def topo_sort(n, edges):
# edges: [(u, v), ...],0-based 点编号
g = [[] for _ in range(n)]
deg = [0] * n
for u, v in edges:
g[u].append(v)
deg[v] += 1
q = deque(i for i in range(n) if deg[i] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in g[u]:
deg[v] -= 1
if deg[v] == 0:
q.append(v)
return order if len(order) == n else None # None 表示有环
C++(链式前向星 + 手写队列,竞赛风):
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 5; // 点数上限,按题目改
const int MAXM = 2e6 + 5; // 边数上限(无向图要 ×2)
int head[MAXN], nxt[MAXM], to[MAXM], w[MAXM];
int deg[MAXN]; // 入度
int q[MAXN]; // 手写队列
int cnt = 0; // 边计数器
void init(int n) {
fill(head, head + n + 1, -1);
fill(deg, deg + n + 1, 0);
cnt = 0;
}
// 有向边 u -> v,权值 c
void add(int u, int v, int c = 0) {
to[cnt] = v;
w[cnt] = c;
nxt[cnt] = head[u];
head[u] = cnt++;
}
// 返回 true 表示是 DAG,order 存拓扑序(长度 n)
bool topoSort(int n, vector<int>& order) {
int h = 0, t = 0;
for (int i = 1; i <= n; ++i)
if (deg[i] == 0) q[t++] = i;
order.clear();
order.reserve(n);
while (h < t) {
int u = q[h++];
order.push_back(u);
for (int i = head[u]; i != -1; i = nxt[i]) {
int v = to[i];
if (--deg[v] == 0) q[t++] = v;
}
}
return (int)order.size() == n;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
init(n);
for (int i = 0; i < m; ++i) {
int u, v, c;
cin >> u >> v >> c; // 不带权就删掉 c
add(u, v, c);
deg[v]++; // 有向图只加被指向端
}
vector<int> order;
if (topoSort(n, order)) {
for (int x : order) cout << x << ' ';
cout << '\n';
} else {
cout << "Cycle\n";
}
return 0;
}
vector<vector<int>>)< 邻接矩阵(n 大时直接爆)。1e6 点 2e6 边,链式前向星稳稳过;vector<vector<int>> 也能过但慢 2~3 倍。// ===== 链式前向星 + Kahn 拓扑排序 模板 =====
const int MAXN = 1e6 + 5, MAXM = 2e6 + 5;
int head[MAXN], nxt[MAXM], to[MAXM], w[MAXM], deg[MAXN], q[MAXN], cnt;
void init(int n) {
fill(head, head + n + 1, -1);
fill(deg, deg + n + 1, 0);
cnt = 0;
}
void add(int u, int v, int c = 0) {
to[cnt] = v; w[cnt] = c; nxt[cnt] = head[u]; head[u] = cnt++;
}
bool topoSort(int n, vector<int>& order) {
int h = 0, t = 0;
for (int i = 1; i <= n; ++i) if (!deg[i]) q[t++] = i;
order.clear(); order.reserve(n);
while (h < t) {
int u = q[h++]; order.push_back(u);
for (int i = head[u]; i != -1; i = nxt[i])
if (--deg[to[i]] == 0) q[t++] = to[i];
}
return (int)order.size() == n;
}
MAXM 按 2m 开,add(u,v) 后必须再 add(v,u),且 deg 两边都加。head 要 fill(-1),deg 要清零,cnt = 0。只清到 n 而不是 MAXN,否则 TLE。for (i = 1; i <= n) 是 1-based;若题目给 0-based,要么整体 +1,要么改循环边界,别混。nxt 和 next 重名:next 在 std 里有,别用 next 当变量名(有的编译器不报错但容易踩)。u == v 的边会让 deg[u] 加 1,拓扑排序时永远出不来 → 正确判成有环,别以为是 bug。order.size() == n 判环:用 int 强转,size() 是 size_t,直接和 n(int)比有符号警告。q[MAXN] 大小按点数开,别按边数。long long:拓扑排序本身不涉及权值累加,但若在拓扑序上做 DP(最长路、方案数),DP 数组要 long long,取模题记得每步取模。priority_queue<int, vector<int>, greater<int>> 替换手写队列,别用 set(常数大)。order.size() == n。dp[v] = max(dp[v], dp[u] + w),在出队 u 遍历出边时更新。cnt[v] = (cnt[v] + cnt[u]) % MOD,同样在遍历出边时累加。level[v] = max(level[v], level[u] + 1)。g[u][v] = 1,拓扑排序时 O(n²) 扫入度为 0 的点,简单粗暴。vector<vector<int>> 存 1e6 点 2e6 边,内存碎片 + 常数大,可能 TLE/MLE,换链式前向星。map<int, vector<int>> 或 list 建图,常数爆炸,竞赛里绝对不要。#define int long long 导致 head 数组翻倍内存、fill 变慢,别用。memset(head, -1, sizeof head) 清整个 MAXN,n 小的时候反而慢;用 fill(head, head + n + 1, -1)。deg 加错端:有向图 u→v 加的是 deg[v],不是 deg[u]。题 1:n=5, m=4,边 1→2, 2→3, 3→4, 4→5。输出任意一个拓扑序,并说明是否唯一。
唯一拓扑序:1 2 3 4 5。因为是一条链,每个点入度依次为 0,1,1,1,1,队列里始终只有一个点,所以唯一。
</details>
题 2:n=3, m=3,边 1→2, 2→3, 3→1。Kahn 算法输出什么?为什么?
输出空(或长度 0 < 3),判定有环。初始所有点入度都是 1,队列为空,循环一次都不进,order.size() == 0 != 3。
</details>
题 3:n=4, m=4,边 1→2, 1→3, 2→4, 3→4,要求输出字典序最小的拓扑序。
用 priority_queue<int, vector<int>, greater<int>>。初始入度 0 的点只有 1,出队 1,2 和 3 入度变 0 入堆;堆顶 2 先出,4 入度变 1;堆顶 3 出,4 入度变 0 入堆;4 出。答案:1 2 3 4。(若用手写队列,可能得到 1 3 2 4,不是字典序最小。)
</details>