一、什么是拓扑排序
拓扑排序是有向无环图(DAG)经典算法,Kahn是工程与竞赛最主流实现,基于贪心思想,易写、能判环、适配DP,本文从概念到代码完整梳理。
1. 适用前提
仅针对有向无环图 DAG,若图中存在环,则不存在合法拓扑序列。
2. 排序规则
给定有向边 u→vu\rightarrow vu→v,在最终拓扑序列中,节点uuu必须出现在节点vvv前面。
- 现实场景:课程选课(先修课排在前面)、任务依赖调度、编译依赖顺序、食物链计数。
- 注意:一张DAG的拓扑序往往不唯一,满足依赖约束即可。
3. 两种主流实现方案
- DFS逆序拓扑:后序遍历反转得到序列,不方便判环、难以控制字典序,日常很少用;
- Kahn入度贪心算法 :首选写法,统计入度+队列迭代,直观易懂,也是本文核心。
二、Kahn算法核心原理(贪心本质)
1. 关键概念:入度
in_degree[x]:指向节点xxx的有向边总数,代表xxx的前置依赖数量。
- 入度 = 0:该节点没有任何前置依赖,可以优先排入拓扑序列。
2. 算法贪心步骤
- 建邻接表存储整张图,预处理所有节点入度;
- 将所有入度为0的节点入队;
- 循环取出队首节点,存入拓扑答案;遍历该点所有出边,把邻接点入度−1-1−1(代表消除一条前置依赖);
- 若某个邻接点入度变为0,送入队列;
- 队列空后校验:答案长度等于总点数→无环;否则图含环,无法拓扑。
贪心逻辑:每次优先选择无依赖的节点,局部最优选点最终得到合法全局拓扑序,和Kruskal选边贪心思路同源。
3. 队列选型区分(高频考点)
- 普通队列deque:不限制字典序,任意合法拓扑序,效率最优;
- 小根堆优先队列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算法优缺点总结
优点
- 可以判环 :最终拓扑数组长度 <n<n<n 即存在环,DFS拓扑不方便快速判环;
- 易结合DP:按拓扑序做状态转移(如P4017食物链计数),是DAGDP标配预处理;
- 灵活控序:更换队列/堆即可控制字典序,适配各类出题要求;
- 代码模板固定,背诵成本低。
缺点
需要额外空间存储入度数组与邻接表,空间开销略大于DFS写法。
五、速记模板口诀
入度统计建邻表,零度节点先入巢;
出队存序消边度,新零入队往复劳;
要字典序用小堆,无序直接用队槽。
拓展延伸
- 求字典序最大拓扑:改用大根堆存储入度为0的节点;
- DAG最长/短路:Kahn跑出拓扑序后顺序DP更新最值。