第十八届蓝桥杯大赛正处于报名季 (软件赛、电子赛报名 2026-10-08---12-18,省赛 2027-04 开赛),全国青少年信息素养大赛算法应用赛项、电子学会 C/C++ 考级(12 月场次报名 9/23---12/2)也都在火热备赛中。这几类赛事的算法题里,有一类「前后顺序约束」问题反复出现:某门课必须先于另一门课学、某个工序必须等前置工序完成、某份文件依赖另一些文件先生成......这类题的本质,就是一个经典图论算法------拓扑排序(Topological Sort)。
本文用一道原创题「校园课程表排课」串起拓扑排序的建模、Kahn 算法、判环、复杂度与进阶,附 C++/Python 双版代码,建议收藏练手。
一、题目目标(原创题)
校园课程表排课
学校开设了
n门课程,编号1 ~ n。部分课程有先修要求:若a必须在b之前学完,记为一条依赖a → b。请你判断:能否安排出一种上课顺序,使所有先修要求都满足(即不存在「绕回自己」的循环依赖)?若能,输出任意一种可行的上课顺序。
这正是蓝桥杯 C/C++ 程序设计、信息素养算法赛项里「依赖 / 工序 / 排队」类题目的标准模型,也是电子学会 C/C++ 考级六级图论基础的必会考点。
- 能排课 ⇔ 依赖关系构成有向无环图(DAG);
- 排不课 ⇔ 依赖关系里存在环(比如 A 依赖 B、B 依赖 C、C 又依赖 A,谁都没法学)。
二、核心考点
- 图建模 :把课程当点、先修依赖当有向边
a → b,存入邻接表。 - 入度(indegree):指向某点的边数。入度为 0 的课「没有任何前置」,可以立刻开课。
- Kahn 算法:用队列不断把「当前入度为 0 的点」取出来排课,并把它的后继入度减一;后继入度降到 0 时再入队。
- 环检测 :若最终排出来的课数
< n,说明还有课永远等不到入度归零------必存在环。 - 可行顺序输出:队列弹出的顺序本身就是一种拓扑序。
三、解法拆解(Kahn 算法)
一句话思路:谁没有前置,谁先上;上完一门,就把它「解锁」的后继往前推一格。
步骤:
-
统计每个点的入度
indeg[v]; -
把所有
indeg == 0的点入队; -
循环:弹出队首
u加入答案,遍历u的每个后继v,令indeg[v]--,若降到 0 则v入队; -
结束时若答案长度等于
n,输出答案;否则报告「存在循环依赖」。
C++ 实现(Kahn)
cpp
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main() {
int n, m;
cin >> n >> m; // n 门课,m 条先修依赖
vector<vector<int>> g(n + 1); // 邻接表,课程编号 1..n
vector<int> indeg(n + 1, 0);
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b; // a 必须在 b 之前:a -> b
g[a].push_back(b);
indeg[b]++;
}
queue<int> q;
for (int i = 1; i <= n; i++)
if (indeg[i] == 0) q.push(i);
vector<int> order;
while (!q.empty()) {
int u = q.front(); q.pop();
order.push_back(u); // 排上这门课
for (int v : g[u]) {
if (--indeg[v] == 0) q.push(v);
}
}
if ((int)order.size() != n) {
cout << "无法排课:存在循环先修依赖\n";
} else {
for (int x : order) cout << x << " ";
cout << "\n";
}
return 0;
}
Python 实现(Kahn)
python
from collections import deque
def topo_sort(n, edges):
g = [[] for _ in range(n + 1)]
indeg = [0] * (n + 1)
for a, b in edges: # a 必须在 b 之前:a -> b
g[a].append(b)
indeg[b] += 1
q = deque(i for i in range(1, n + 1) if indeg[i] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return order if len(order) == n else None # None 表示有环
n, m = map(int, input().split())
edges = [tuple(map(int, input().split())) for _ in range(m)]
res = topo_sort(n, edges)
print("无法排课:存在循环先修依赖" if res is None else " ".join(map(str, res)))
样例 :n = 5,依赖 (1,2) (1,3) (2,4) (3,4) (4,5),运行得到可行顺序 1 2 3 4 5。
有环样例 :(1,2) (2,3) (3,1),程序输出「无法排课」,因为谁都不满足入度为 0。
四、对比:DFS 三色标记判环
除了 Kahn,还可以用深度优先搜索 + 三色标记判环:白=未访问、灰=正在访问(在递归栈中)、黑=已访问完。若在 DFS 中遇到「灰色」邻居,说明从它又走回了自己------即存在环。
python
def has_cycle(n, edges):
g = [[] for _ in range(n + 1)]
for a, b in edges:
g[a].append(b)
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * (n + 1)
def dfs(u):
color[u] = GRAY
for v in g[u]:
if color[v] == GRAY: # 回到正在访问的祖先 => 环
return True
if color[v] == WHITE and dfs(v):
return True
color[u] = BLACK
return False
return any(color[i] == WHITE and dfs(i) for i in range(1, n + 1))
- Kahn:额外得到一种拓扑序,代码直观,判环顺带完成;
- DFS 三色:不依赖队列、不改图结构,判定「有没有环」更紧凑,但想要具体顺序要再跑一遍。
五、时间与空间复杂度
- 时间 :每个点入队/出队一次,每条边被扫描一次 → O(V + E)(V 为课程数,E 为依赖数)。
- 空间 :邻接表、入度数组、队列各 O(V + E) → O(V + E)。
对比「盲目枚举全排列再校验」(最坏 O(V!·E)),拓扑排序把指数级暴力直接压成线性,这正是它被赛事青睐的原因。
六、易错点(6 个,考试最容易丢分)
- 入度统计漏边 / 重边 :
a → b和a → b是两条边时,indeg[b]要 +2;用map去重或按原始边计数,二者不要混。 - 队列空后不校验点数 :只写「输出队列内容」却忘了判断
order.size() == n,有环时也会输出部分顺序,判题直接判错。 - 自环 :
a → a这种依赖,入度永远不为 0,必须靠「点数不等于 n」兜住,不能假设输入无自环。 - 多个连通块 :图可能分成几块互不相连,初始化入度 0 的点要全部入队,而不是只入队第一个。
- 编号偏移 :题目常给 1-indexed,数组却开成
n大小(应为n+1),导致最后一门课越界。 - 顺序 ≠ 唯一:拓扑序本来就有很多种,判题通常只验「是否合法」,别纠结和样例顺序「长得不一样」。
七、进阶
① 字典序最小的拓扑序
把队列换成小根堆(优先队列),每次弹当前编号最小的入度为 0 的点即可。注意堆里只放「刚降到 0」的点,不是每次全图扫一遍。
python
import heapq
def topo_min_lex(n, edges):
g = [[] for _ in range(n + 1)]
indeg = [0] * (n + 1)
for a, b in edges:
g[a].append(b); indeg[b] += 1
heap = [i for i in range(1, n + 1) if indeg[i] == 0]
heapq.heapify(heap)
order = []
while heap:
u = heapq.heappop(heap)
order.append(u)
for v in g[u]:
indeg[v] -= 1
if indeg[v] == 0:
heapq.heappush(heap, v)
return order if len(order) == n else None
样例 (1,2)(1,3)(2,4)(3,4)(4,5) 会得到最小字典序 1 2 3 4 5。
② 拓扑排序 + DP:求「最长课程链」
若问「最少需要几个学期,每学期可并行上多门无依赖的课」,可令 dist[v] = max(dist[u]+1),在 Kahn 松弛时顺手更新,最终 max(dist) 就是答案。样例最长链为 4(如 1→2→4→5)。
③ 唯一拓扑序判定 :任意时刻队列/堆里恰好只有 1 个入度为 0 的点,拓扑序才唯一;否则有多种。
④ 输出所有拓扑序:回溯枚举每次可选的入度 0 集合,常用于「全排列型」搜索题。
八、小结与互动
拓扑排序是图论里性价比极高的一招:Kahn 算法 O(V+E) 线性搞定「依赖排序 / 工序安排 / 判环」,也是后续关键路径、拓扑 DP、编译依赖、任务调度的基石。蓝桥杯的「排队打水变形」、信息素养的「流程依赖」、电子学会 C/C++ 考级的图论章节,都绕不开它。
👉 你练手时遇到过「拓扑序不唯一」「字典序最小」这类变体吗?或者想看下一期拆解哪个算法(并查集进阶 / 最短路径 / 动态规划)?欢迎在评论区留言,下一篇就写它!
如果觉得有用,点赞 + 收藏 + 关注,带你用一篇一个算法的方式啃完少儿编程赛事高频考点。
📚 免费少儿编程资料(夸克网盘领取)
以下资料来自夸克网盘分享,点击链接可直接保存;若需在 App 内打开,也可复制下方明文链接:
- 全国青少年信息素养大赛复赛集训题目Python&C++.docx
https://pan.quark.cn/s/93995d3cb150 - 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
https://pan.quark.cn/s/da97b5dbf75d - Python背记手册.pdf
https://pan.quark.cn/s/7568ae9ca92b - Python课程
https://pan.quark.cn/s/a94bf02d00c6 - 2024信息素养大赛图形化复赛集训题答案3-9
https://pan.quark.cn/s/6ccab7ec3cbc - 2025年03月份电子学会考级真题
https://pan.quark.cn/s/4403c4228912 - 2025全国青少年信息素养大赛赛项说明
https://pan.quark.cn/s/d9d0df4a9f29 - 青少儿信息素养大赛编程资料
https://pan.quark.cn/s/4ab6bd83be8a
资料持续更新,关注获取最新分享。