拓扑排序超详解:原理 + Kahn 贪心算法

一、什么是拓扑排序

拓扑排序是有向无环图(DAG)经典算法,Kahn是工程与竞赛最主流实现,基于贪心思想,易写、能判环、适配DP,本文从概念到代码完整梳理。

1. 适用前提

仅针对有向无环图 DAG,若图中存在环,则不存在合法拓扑序列。

2. 排序规则

给定有向边 u→vu\rightarrow vu→v,在最终拓扑序列中,节点uuu必须出现在节点vvv前面

  • 现实场景:课程选课(先修课排在前面)、任务依赖调度、编译依赖顺序、食物链计数。
  • 注意:一张DAG的拓扑序往往不唯一,满足依赖约束即可。

3. 两种主流实现方案

  1. DFS逆序拓扑:后序遍历反转得到序列,不方便判环、难以控制字典序,日常很少用;
  2. Kahn入度贪心算法首选写法,统计入度+队列迭代,直观易懂,也是本文核心。

二、Kahn算法核心原理(贪心本质)

1. 关键概念:入度

in_degree[x]:指向节点xxx的有向边总数,代表xxx的前置依赖数量。

  • 入度 = 0:该节点没有任何前置依赖,可以优先排入拓扑序列。

2. 算法贪心步骤

  1. 建邻接表存储整张图,预处理所有节点入度;
  2. 将所有入度为0的节点入队;
  3. 循环取出队首节点,存入拓扑答案;遍历该点所有出边,把邻接点入度−1-1−1(代表消除一条前置依赖);
  4. 若某个邻接点入度变为0,送入队列;
  5. 队列空后校验:答案长度等于总点数→无环;否则图含环,无法拓扑。

贪心逻辑:每次优先选择无依赖的节点,局部最优选点最终得到合法全局拓扑序,和Kruskal选边贪心思路同源。

3. 队列选型区分(高频考点)

  1. 普通队列deque:不限制字典序,任意合法拓扑序,效率最优;
  2. 小根堆优先队列heapq :题目要求字典序最小拓扑,每次优先取出编号最小的入度0节点。

三、洛谷B3644【模板】拓扑排序/家谱树

题目链接:https://www.luogu.com.cn/problem/B3644

题意简述

家谱树构成DAG,父节点在前、子节点在后,输出字典序最小的拓扑序列

需求字典序最小 → 不能用普通队列,改用小根堆;你提供的原始代码是普通队列写法,适配无字典序要求的拓扑,下文附带两种版本代码。

代码1:普通队列版(无字典序要求)

python 复制代码
import sys
input = lambda: sys.stdin.readline().strip()
from collections import deque

n = int(input())
inde = [0] * n
e = [[] for _ in range(n)]
# 建图,输入转0下标
for u in range(n):
    a = list(map(int, input().split()))
    a = [x-1 for x in a]
    # 最后一个0是终止标识,舍弃
    e[u].extend(a[:len(a)-1])
    for v in e[u]:
        inde[v] += 1

dq = deque()
# 入度为0入队
for idx, val in enumerate(inde):
    if val == 0:
        dq.append(idx)

ans = []
while dq:
    u = dq.popleft()
    ans.append(u+1) # 转回题目1下标输出
    for v in e[u]:
        inde[v] -= 1
        if inde[v] == 0:
            dq.append(v)

print(' '.join(map(str, ans)))

代码2:小根堆版(满足B3644字典序最小要求)

python 复制代码
import sys, heapq
input = lambda: sys.stdin.readline().strip()

n = int(input())
inde = [0] * n
e = [[] for _ in range(n)]
for u in range(n):
    a = list(map(int, input().split()))
    a = [x-1 for x in a]
    e[u].extend(a[:-1])
    for v in e[u]:
        inde[v] += 1

heap = []
for i in range(n):
    if inde[i] == 0:
        heapq.heappush(heap, i)

ans = []
while heap:
    u = heapq.heappop(heap)
    ans.append(u+1)
    for v in e[u]:
        inde[v] -= 1
        if inde[v] == 0:
            heapq.heappush(heap, v)

print(' '.join(map(str, ans)))

四、Kahn算法优缺点总结

优点

  1. 可以判环 :最终拓扑数组长度 <n<n<n 即存在环,DFS拓扑不方便快速判环;
  2. 易结合DP:按拓扑序做状态转移(如P4017食物链计数),是DAGDP标配预处理;
  3. 灵活控序:更换队列/堆即可控制字典序,适配各类出题要求;
  4. 代码模板固定,背诵成本低。

缺点

需要额外空间存储入度数组与邻接表,空间开销略大于DFS写法。

五、速记模板口诀

入度统计建邻表,零度节点先入巢;

出队存序消边度,新零入队往复劳;

要字典序用小堆,无序直接用队槽。

拓展延伸

  1. 求字典序最大拓扑:改用大根堆存储入度为0的节点;
  2. DAG最长/短路:Kahn跑出拓扑序后顺序DP更新最值。
相关推荐
阿图灵5 小时前
OpenCV 实用技巧三连:视频转图片、图片转视频、Pillow 与 OpenCV 互转
图像处理·python·opencv·计算机视觉·音视频·pillow
GlueNa2SiO35 小时前
05-Flask表单处理与文件上传
笔记·python·学习·flask
郝学胜-神的一滴5 小时前
C++20 高级编程 004:从初始化、内存到const系列关键字
开发语言·算法·编程·软件构建·c++20
一晌小贪欢5 小时前
Python办公16:PDF 强力缝合——将几十个 PDF 合并为单个文档并添加页码
开发语言·python·excel·数据可视化·python办公
qq_161111275 小时前
深入理解Python中的Contextlib库
python·装饰器·函数·上下文管理器·contextlib
kyle~5 小时前
点云配准--- 迭代最近点 ICP 求解
线性代数·算法·机器学习
迷迭香yy5 小时前
回测过拟合检测体系从样本内外到组合稳健性评估 IG50免费开源股票数据API接口
服务器·开发语言·数据库·人工智能·python
Zane1994125 小时前
`ClassName()` 只是一步?拆开看 `__new__` 和 `__init__` 各自在干什么
开发语言·python
qq7422349845 小时前
Gradio 极简入门:三分钟为AI模型打造交互界面,并对比Streamlit与Dash如何选型
人工智能·算法·大模型·交互·dash
GlueNa2SiO36 小时前
04-Flask数据库操作SQLAlchemy
笔记·python·flask