NeFut Logo NeFut
EN 管理员登录

深入解析拓扑排序:Kahn算法及其优化

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #C++ #Topological Sort

核心逻辑与数学原理

拓扑排序(Topological Sort)是一种对有向无环图(Directed Acyclic Graph, DAG)顶点进行线性排序的算法。排序后,对于图中的任意一条有向边 $(u, v)$,顶点 $u$ 必然在排序中出现在顶点 $v$ 的前面。

在 NOIP 体系下,拓扑排序的核心代数原理与状态控制包含以下三个要素:

1. 入度驱动范式:Kahn 算法(广度优先搜索变形)

Kahn 算法的本质是基于消除零入度节点的动态拓扑收敛

2. 环路判定的拓扑判据

如果图中存在有向环路(如 $A o B o C o A$),环内所有节点的入度在外界松弛耗尽后,永远无法降为 $0$。因此,它们绝对不可能进入零入度队列。

3. 全局字典序最小的拓扑链选择

在许多竞赛实际场景中,满足拓扑关系的位置往往不唯一。若题目强制要求输出全局字典序最小的拓扑序列,传统的 FIFO 队列(std::queue)将无法胜任。


算法推导与状态设计

1. Kahn 算法状态设计

定义一维计数数组 in_degree[i],在读入边 $(u, v)$ 时同步执行:in_degree[v]++

  1. 初始化:扫描整个图,将所有满足 in_degree[i] == 0 的节点压入优先队列中。
  2. 状态转移方程: 对于当前出堆的零入度节点 $u$,遍历其邻接表中的出边。对于每个终点 $v$:

$$ ext{in ext{_}degree}[v] ightarrow ext{in ext{_}degree}[v] - 1$$

$$ ext{if } ( ext{in ext{_}degree}[v] == 0) ext{ pq.push}(v)$$


C++ 标准源码(全局字典序最小拓扑排序模板)

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

const int MAXN = 100005;

std::vector<int> adj[MAXN]; // 邻接表存储有向图
int in_degree[MAXN];       // 记录每个节点的入度
std::vector<int> topo_res;  // 存储最终生成的拓扑序列

bool topological_sort(int n) {
    // 核心战术:使用小根堆确保当前所有入度为 0 的节点中,编号最小的优先出队
    std::priority_queue<int, std::vector<int>, std::greater<int>> pq;

    // 1. 全局扫描,将初始入度为 0 的点并入状态堆
    for (int i = 1; i <= n; ++i) {
        if (in_degree[i] == 0) {
            pq.push(i);
        }
    }

    // 2. 拓扑迭代扩张
    while (!pq.empty()) {
        int u = pq.top();
        pq.pop();
        topo_res.push_back(u); // 归并入最终拓扑序列

        // 遍历以 u 为起点的所有出边
        for (size_t i = 0; i < adj[u].size(); ++i) {
            int v = adj[u][i];
            in_degree[v]--; // 逻辑切断边 $(u, v)$,终点入度减 1

            // 必须严格判定 == 0 的瞬间
            if (in_degree[v] == 0) {
                pq.push(v);
            }
        }
    }

    // 3. 环路判定代数校验
    // 若最终生成的序列点数不等于总点数,说明图中有向图存在环路,拓扑链断裂
    return topo_res.size() == static_cast<size_t>(n);
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m;
    if (!(std::cin >> n >> m)) return 0;

    for (int i = 0; i < m; ++i) {
        int u, v;
        std::cin >> u >> v;
        adj[u].push_back(v); // 建立有向边 u -> v
        in_degree[v]++;      // 终点 v 添加入度
    }

    if (!topological_sort(n)) {
        std::cout << -1 << "\n"; // 图中包含有向环,拓扑排序失败
    } else {
        for (int i = 0; i < n; ++i) {
            std::cout << topo_res[i] << (i == n - 1 ? "" : " ");
        }
        std::cout << "\n";
    }

    return 0;
}

NOIP 实战避坑指南

  1. 混淆“字典序最小的拓扑序”与“反向拓扑序”: 这是竞赛题中极其经典的一个大坑。有些题目要求“让编号小的节点尽量靠前”,这在字面直觉上很容易让人觉得直接用小根堆跑 Kahn 算法就行。但实际上,“字典序最小”和“让小编号尽量靠前”在数学上是不等价的(例如:序列 $[1, 4, 2, 3]$ 的字典序小于 $[2, 1, 3, 4]$,但是后者的 1 更加靠前)。
  1. 入度减 1 的条件拦截写错: 在松弛邻接点时,判断语句必须严格写成 if (in_degree[v] == 0)。如果贪图省事写成 if (in_degree[v] <= 0),虽然在绝大多数正常图上没有区别,但如果原图由于某种建图变形导致初始包含了入度为负数(错误状态)或者发生并发逻辑,由于重复压入堆结构会造成算法出现逻辑漏洞。

经典 NOIP/洛谷 真题

1. 洛谷 P1347 排序

2. Kahn P4017

原文链接: local://12.1

[h] 返回首页