拓扑排序超详解:原理 + 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更新最值。
相关推荐
LSssT.1 小时前
【01】Python 机器学习
开发语言·python
为爱停留1 小时前
给智能体装上「刹车」:中断(Interrupts)与人工审批全解析
python
三品吉他手会点灯2 小时前
C语言学习笔记 - 43.运算符与表达式 - 运算符1 - 运算符的分类和简单介绍
c语言·笔记·学习·算法
l1t2 小时前
DeepSeek总结的使用实体-组件-系统和基于存在性处理进行Python编程39-40
开发语言·python
VkN2X2X4b2 小时前
算法复杂度的实验验证与误差分析的技术8
算法
其利天下技术2 小时前
风扇灯无刷电机自适应算法实战指南
算法·cocos2d·无刷电机自适应算法·bldc驱动自适应算法·其利无刷电机驱动算法
8Qi82 小时前
LeetCode 494:目标和(Target Sum)—— 题解 ✅
算法·leetcode·职场和发展·动态规划·01背包
曾阿伦2 小时前
Python 搭建简易HTTP服务
开发语言·python·http
MIUMIUKK2 小时前
从语法层面,看懂 Python 的特殊处
java·开发语言·python