蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:拓扑排序课程表排课解析

第十八届蓝桥杯大赛正处于报名季 (软件赛、电子赛报名 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,谁都没法学)。

二、核心考点

  1. 图建模 :把课程当点、先修依赖当有向边 a → b,存入邻接表。
  2. 入度(indegree):指向某点的边数。入度为 0 的课「没有任何前置」,可以立刻开课。
  3. Kahn 算法:用队列不断把「当前入度为 0 的点」取出来排课,并把它的后继入度减一;后继入度降到 0 时再入队。
  4. 环检测 :若最终排出来的课数 < n,说明还有课永远等不到入度归零------必存在环。
  5. 可行顺序输出:队列弹出的顺序本身就是一种拓扑序。

三、解法拆解(Kahn 算法)

一句话思路:谁没有前置,谁先上;上完一门,就把它「解锁」的后继往前推一格。

步骤:

  1. 统计每个点的入度 indeg[v];

  2. 把所有 indeg == 0 的点入队;

  3. 循环:弹出队首 u 加入答案,遍历 u 的每个后继 v,令 indeg[v]--,若降到 0 则 v 入队;

  4. 结束时若答案长度等于 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 个,考试最容易丢分)

  1. 入度统计漏边 / 重边 :a → b 和 a → b 是两条边时,indeg[b] 要 +2;用 map 去重或按原始边计数,二者不要混。
  2. 队列空后不校验点数 :只写「输出队列内容」却忘了判断 order.size() == n,有环时也会输出部分顺序,判题直接判错。
  3. 自环 :a → a 这种依赖,入度永远不为 0,必须靠「点数不等于 n」兜住,不能假设输入无自环。
  4. 多个连通块 :图可能分成几块互不相连,初始化入度 0 的点要全部入队,而不是只入队第一个。
  5. 编号偏移 :题目常给 1-indexed,数组却开成 n 大小(应为 n+1),导致最后一门课越界。
  6. 顺序 ≠ 唯一:拓扑序本来就有很多种,判题通常只验「是否合法」,别纠结和样例顺序「长得不一样」。

七、进阶

① 字典序最小的拓扑序

把队列换成小根堆(优先队列),每次弹当前编号最小的入度为 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 内打开,也可复制下方明文链接:

  1. 全国青少年信息素养大赛复赛集训题目Python&C++.docx
    https://pan.quark.cn/s/93995d3cb150
  2. 2024信息素养-智能算法应用挑战赛-复赛初中组题目7月7日.pdf
    https://pan.quark.cn/s/da97b5dbf75d
  3. Python背记手册.pdf
    https://pan.quark.cn/s/7568ae9ca92b
  4. Python课程
    https://pan.quark.cn/s/a94bf02d00c6
  5. 2024信息素养大赛图形化复赛集训题答案3-9
    https://pan.quark.cn/s/6ccab7ec3cbc
  6. 2025年03月份电子学会考级真题
    https://pan.quark.cn/s/4403c4228912
  7. 2025全国青少年信息素养大赛赛项说明
    https://pan.quark.cn/s/d9d0df4a9f29
  8. 青少儿信息素养大赛编程资料
    https://pan.quark.cn/s/4ab6bd83be8a

资料持续更新,关注获取最新分享。

相关推荐
const_qiu1 小时前
第三章 04:3.6 基础调用链的构建
开发语言·python
向上的车轮1 小时前
Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南
开发语言·python
程序员小孔.1 小时前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序
暖焰核心1 小时前
二叉搜索树——从插入查到key-value改写
c++
程序猿乐锅2 小时前
【从0-1学习Python | 第六篇】 Python 函数:定义、参数与返回值
开发语言·python·学习
MasonYyp3 小时前
声音转文字的模型
python
草莓熊Lotso5 小时前
【C++ 基于微服务的即时通讯系统】etcd + brpc 从安装到服务注册发现全链路实战
网络·数据库·c++·redis·微服务·etcd
信誓旦旦的程序猿10 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河10 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven