算法讲解059 必备 建图、链式前向星、拓扑排序

来源:算法讲解059【必备】建图、链式前向星、拓扑排序 · 2026-10-25
依据:视频字幕 2000 字

一句话概括

用三种方式(邻接矩阵、邻接表、链式前向星)把图存进计算机,再用拓扑排序(Topological Sort)对有向无环图(DAG)求出一个线性顺序。

它解决什么问题

基础知识(前置概念,用大白话解释)

原理(为什么这么做是对的,尽量给直觉)

三种建图方式的直觉:

拓扑排序为什么对(Kahn 算法):

DAG 里一定存在入度为 0 的点(否则沿着入边一直往回走必然成环)。把入度为 0 的点拿出来放进答案,然后「删掉」它的所有出边——等价于把它指向的点的入度减 1。减到 0 的点说明它所有前驱都已经排进答案了,可以安全入队。反复做,直到队列空。

直觉:每次取「当前没有任何未完成前置任务」的任务,这是唯一合法的执行顺序来源。

步骤(编号,可照着写)

以链式前向星 + Kahn 拓扑排序为例:

  1. 读入 n(点数)、m(边数)。点编号按题目,0-based 或 1-based 都行,全篇统一。
  2. 开数组:head[n] 初始化为 -1;to[m]、nxt[m]、w[m](带权才要);deg[n] 入度清零;cnt = 0 边计数器。
  3. 加边 add(u, v, w):

- to[cnt] = v; w[cnt] = w; nxt[cnt] = head[u]; head[u] = cnt++;

- 若是无向图,再 add(v, u, w) 一次。

- 同时 deg[v]++(有向图只加被指向那端)。

  1. 把所有 deg[i] == 0 的点入队(手写数组队列)。
  2. 循环:出队 u,把 u 放进答案序列;遍历 u 的所有出边 i = head[u] ... i = nxt[i],对 v = to[i] 做 if (--deg[v] == 0) q[tail++] = v;。
  3. 结束后若答案长度 == n,输出答案;否则报告有环。

样例(一个具体小例子,带每步变化)

有向图,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 一份:好读、用于理解;C++ 一份:竞赛风,能直接抄进比赛,带注释,不用玄学宏)

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;
}

复杂度(时间/空间,最好/平均/最坏,顺带说常数大不大)

竞赛里什么时候用(看到什么题面特征就该想到它)

竞赛模板(C++,可直接背下来的写法;变量名清楚)

// ===== 链式前向星 + 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;
}

边界与易错点(WA 的常见原因,越具体越好)

  1. 无向图边数要开两倍:MAXM 按 2m 开,add(u,v) 后必须再 add(v,u),且 deg 两边都加。
  2. 多测清空:head 要 fill(-1),deg 要清零,cnt = 0。只清到 n 而不是 MAXN,否则 TLE。
  3. 点编号 0-based 还是 1-based:模板里 for (i = 1; i <= n) 是 1-based;若题目给 0-based,要么整体 +1,要么改循环边界,别混。
  4. nxt 和 next 重名:next 在 std 里有,别用 next 当变量名(有的编译器不报错但容易踩)。
  5. 自环:u == v 的边会让 deg[u] 加 1,拓扑排序时永远出不来 → 正确判成有环,别以为是 bug。
  6. 重边:链式前向星天然支持重边,不用去重;但如果题目要求去重(如最短路),要自己处理。
  7. order.size() == n 判环:用 int 强转,size() 是 size_t,直接和 n(int)比有符号警告。
  8. 队列开在全局:q[MAXN] 大小按点数开,别按边数。
  9. long long:拓扑排序本身不涉及权值累加,但若在拓扑序上做 DP(最长路、方案数),DP 数组要 long long,取模题记得每步取模。
  10. 字典序最小拓扑序:用 priority_queue<int, vector<int>, greater<int>> 替换手写队列,别用 set(常数大)。

同类题 / 变形(说清楚怎么改)

常见坑

拓展(变体、实际用途、面试常问)

自测 3 题(题目 + 折叠答案)

题 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>