01 / 本课学习路线
本课学习路线
预计约 150 分钟,具体时长根据待订正题目数量调整
02 / 订正流程与完成条件
订正不是重做一遍:四个动作,每道错题各走一次
「再做一遍」不是修复。订正的定义是:说清第一次错在哪一类、用一个最小用例复现它、改掉它、证明改对了、再不看旧代码重写一次。模块 1、2 的错题订正已经走过两遍,本课把同一套流程用在本模块的搜索与图题上。
| 动作 | 做什么 | 产出 | 对应正文 |
|---|---|---|---|
| ① 分类 | 按第一次出错的原因归到六类之一 | 登记表的「首次错误类型」一栏 | 第 03 节 |
| ② 定位并修复 | 找到导致错误的那几行代码或那条被漏读的规则,改掉 | 登记表的「错误原因」「修复动作」 | 第 03、04 节 |
| ③ 最小验证 | 构造一个只触发该错误的输入,修复前失败、修复后通过 | 登记表的「触发用例」「最小验证」 | 第 04、05 节 |
| ④ 独立重写 | 不看旧代码从空文件重写整题,用复测清单验证后提交 | 登记表的「独立重写」 | 第 06 节 |
完成条件:本课没有新的必做题;下面的「重做任务」是复习题,用于独立重写,它们的判题结果不直接参与完成判定。本课算完成 = 勾选全部四条「学习完成检查」,其中第 3 条要求本模块 2–3 道用时较长或错误较多的必做题已从空文件独立重写,第 4 条要求未通过的题全部订正。暂时订正不了的题可以在登记表写明具体问题、先进入下一模块学习,但对应的检查项要等订正完成后再勾选。先用触发原错误的输入检查修改,再验证边界和题面样例;这些检查通过后,再独立重写并提交。
| 来源 | 题号 | 判题结果 | 先猜的错误类型 | 订正状态 |
|---|---|---|---|---|
| 阶段测验第 1 题 | P2608 | 答案错误(WA) / 运行错误(RE) / 未完成 / 通过(AC) | — | 未开始 |
| 阶段测验第 2 题 | P3497 | 同上 | — | 未开始 |
| 阶段测验第 3 题 | P3755 | 同上 | — | 未开始 |
| 第 1~5 课 未通过或看提示才通过的必做题 / 进阶题 | 逐题填 | 填判题结果 | — | 未开始 |
「先猜的错误类型」在第 03 节按判定信号确认后再改;AC 的题只登记曾经改过的具体问题,不进入订正流程。看提示才通过的题按「未通过」处理——提示告诉你的那一步,就是要独立重写验证的那一步。
03 / 六类错误的判定与定位
判定信号、定位步骤、修复动作、最小用例怎么构造(本模块版)
分类不靠感觉,靠信号:先看判题结果是哪一种,再用下面的问题逐个排除。本模块的题多了「图怎么建」这一层——边的方向、起点集合、标记时机、栈里存什么——它们的错误几乎都落在建模与边界两类。
| 类型 | 判定信号 | 定位步骤 | 修复动作 | 最小用例怎么构造 |
|---|---|---|---|---|
| 读题 | 样例能过、某些用例 WA;对「更高是否严格」「方向」「起点算不算」的理解与题目页不一致 | 重新写题目要求清单:比较是否含等号、起点是否计入答案、边的方向、并列规则、无解输出;逐条对照代码 | 按那一条改一处 | 构造只考那一条规则的输入(案例 A:三人等高) |
| 建模 | 所有用例都错或大面积错;说不清栈里存什么、队列第 0 层是谁、dist 有几维 | 把变量含义写在纸上,用题面示例逐步手推(第 1 课 的栈表、第 2 课 的逐层表、第 4 课 的入度表、第 5 课 的出堆表) | 改栈的内容 / 起点集合 / 边方向 / 状态维度那一处 | 取题面示例或比它更短的输入(案例 B:两个源点的 3×3) |
| 边界 | 只有特定输入错:空图、单点、自环、重边、不连通、被围住 | 列出四类边界输入逐个手算 | 补上判断或改判据(拓扑序长度、待改造格数) | 就是那个边界输入本身 |
| 复杂度 | 超时(TLE),且不是读入问题 | 估算最坏次数:出队才标记、list.pop(0)、每轮扫全体找入度 0、不跳过过期条目 | 换成入队即标记 / deque / 减到 0 入队 / 懒删除 | 构造最大规模输入计时 |
| 语言 | 运行错误(RE)或结果对不上却找不到逻辑错 | 写 3 行小实验:空列表取 [-1]、递归上限、deque 空时 popleft、元组比较 | 换成明确的写法(先判空再取栈顶、sys.setrecursionlimit) | 触发该语言行为的最短输入(案例 C:)() |
| 输出格式 | 答案错误(WA)或格式错误(PE),数字都对 | 把自己的输出与期望输出逐字节对比 | 改分隔符、行数、编号基准(P2650 下标从 0 起) | 题面示例 |
登记时的一条硬要求:「错误原因」必须具体到能据此直接改代码——「粗心」「没注意」不符合要求,「只让第一个 YES 入队,其余源点被当成普通格子」符合。写不出具体原因,说明还没定位到,回到定位步骤。
04 / 三个完整订正案例
失败用例 → 定位 → 修改前后对照 → 复测
三个案例都来自本模块第 1、2 课 必做题里真实会出现的错误,每个都走完整流程;你的错题按同样的格式处理。
案例 A · 读题类:P2650 弹栈条件写成了 >=
判题结果: 答案错误(WA);题面示例能过(结算时没有遇到等高的人) 失败用例: 3 / 170 170 170 期望 0 0 0 实际 1 2 0 定位: 重写题目要求清单 → 「比自己高」是严格更高,等高的人不是朋友;代码里弹栈条件写成 >=,等高的人也被结算 修复: 弹栈条件改成严格大于(见下方前后对照) 复测: 3 / 170 170 170 → 0 0 0 ✓ 3 / 170 171 172 → 1 2 0 ✓(严格递增,两种写法结果相同) 题面示例 8 / 123 124 125 121 119 122 126 123 → 1 2 6 5 5 6 0 0 ✓
案例 A 修改前(片段)
Python# P2650 修改前:弹栈条件写成了 >=——等高的人也被当成「更高」结算
while stack and h >= heights[stack[-1]]:
j = stack.pop()
ans[j] = i案例 A 修改后(片段)
Python# P2650 修改后:题面「比自己高」→ 严格更高才结算,等高的人继续留在栈里
while stack and h > heights[stack[-1]]: # 严格更高才结算
j = stack.pop()
ans[j] = i # 题面示例证明输出的是下标(从 0 起)只改了比较符;完整程序在第 1 课 第 08 节展开区,单调栈推演表在第 1 课 第 05 节。修改前后各跑一次三个复测用例,修改前第一个用例失败、修改后全部通过——这才是「修复生效」。
案例 B · 建模类:P3702 只让第一个 YES 入队(单源)
判题结果: 答案错误(WA);只有一个宜居区的用例能过 失败用例: YES NO NO / NA NO NO / NO NO YES 期望 2 实际 4 定位: 用第 2 课 第 04 节的逐天推演表手推:第 0 层队列应是 (0,0) 和 (2,2) 两个源点;代码只把第一个 YES 入队,(2,2) 附近的格子要从 (0,0) 绕过来,多走两层——起点集合写错是方法本身错,不是某个特例 修复: 所有 YES 在第 0 层一起入队(见下方前后对照) 复测: 上面的 3×3 → 2 ✓ YES NO NO → 2 ✓(单个源点,两种写法结果相同) 题面示例 YES YES NO / NO NO NO / NA NO YES → 1 ✓
案例 B 修改前(片段)
Python# P3702 修改前:只让第一个 YES 入队——其余宜居区被当成普通格子,第 0 层只有一个源点
for r in range(rows):
for c in range(cols):
if grid[r][c] == "YES":
if not q:
q.append((r, c))
elif grid[r][c] == "NO":
todo += 1案例 B 修改后(片段)
Python# P3702 修改后:所有宜居区同时扩散 → 全部 YES 都在第 0 层入队
for r in range(rows):
for c in range(cols):
if grid[r][c] == "YES":
q.append((r, c)) # 所有宜居区都是第 0 层
elif grid[r][c] == "NO":
todo += 1建模类错误的定位工具是「逐步手推表」:把第 0 层队列里有谁写出来,与程序的实际行为对照,第一处不一致就是要改的地方。完整程序在第 2 课 第 08 节展开区。
案例 C · 语言类:P2600 右括号来时没判空栈
判题结果: 运行错误(RE);左括号先出现的输入都能过
失败用例: )( 期望 0 实际 IndexError
定位: 写 3 行小实验:空列表取 [-1] 和 pop() 都抛 IndexError——右括号先来时栈是空的,stack[-1] 越界;题面要求这种输入输出 0
修复: 先判空栈再看栈顶(见下方前后对照)
复测: )( → 0 ✓ (] → 0 ✓(栈非空但类型不配对) 题面示例 ([]{()}) → 3 ✓案例 C 修改前(片段)
Python# P2600 修改前:右括号来时直接看栈顶——栈空时 stack[-1] 抛 IndexError
else:
if stack[-1] != match.get(ch):
valid = False
break
stack.pop()案例 C 修改后(片段)
Python# P2600 修改后:先判空栈,再看栈顶类型是否配对
else:
if not stack or stack[-1] != match.get(ch): # 栈空、或栈顶类型不配对
valid = False
break
stack.pop()语言类错误的最小用例是「触发该语言行为的最短输入」:两个字符就够。完整程序在第 1 课 第 08 节展开区。
05 / 错题登记与最小验证
登记模板、填好的示例、最小用例的三个要求
登记表是订正的账本:每道错题一条,订正完把「独立重写」一栏改成「通过」。下面先给空模板,再给案例 B 填好的样子。
错题登记模板(每题一条)
Python# 错题登记(每题一条;订正完把「独立重写」改成「通过」)
# 题号:
# 来源: 阶段测验第_题 / 课_ 必做 / 课_ 进阶
# 判题结果: WA / RE / TLE / PE
# 首次错误类型: 读题 / 建模 / 边界 / 复杂度 / 语言 / 输出格式
# 触发用例: 输入=____ 期望=____ 实际=____
# 错误原因: (具体到能据此直接改代码)
# 修复动作:
# 最小验证: (至少三条:触发错误的、边界另一侧的、题面示例)
# 独立重写: 未做 / 通过 / 再错(类型=__)复制进你的错题本,每订正一项填一条;如果暂时无法准确描述错误原因,请结合失败用例和代码位置进一步检查,再回到六类错误原因重新分类。
填好的示例(案例 B)
Python# 题号: P3702
# 来源: 第 2 课 必做
# 判题结果: WA
# 首次错误类型: 建模
# 触发用例: 输入=YES NO NO / NA NO NO / NO NO YES 期望=2 实际=4
# 错误原因: 只把第一个 YES 入队,第 0 层只有 (0,0) 一个源点;(2,2) 这个宜居区被当成普通格子,它附近的 NO 要从 (0,0) 绕过来
# 修复动作: 建队列时把所有 YES 都入队(多源 BFS 的第 0 层是全部源点)
# 最小验证: YES NO NO / NA NO NO / NO NO YES → 2 ✓ YES NO NO → 2 ✓(单个源点) YES YES NO / NO NO NO / NA NO YES → 1 ✓(题面示例)
# 独立重写: 通过「触发用例」写明期望与实际;「错误原因」写到能直接改代码;「最小验证」至少三条:触发错误的、边界另一侧的、题面示例。
| 要求 | 含义 | 反例 |
|---|---|---|
| 只触发这一类错误 | 修复前失败、修复后通过,且不涉及其它规则 | 拿 4×10⁴ 人验证「更高是否严格」 |
| 尽量短 | 手算能在 1 分钟内得到期望输出 | 用 500×500 地图验证「源点有没有全部入队」 |
| 带期望输出 | 登记时写清期望与实际 | 只写「输入 3 / 170 170 170」不写期望 |
每订正一项做一次最小验证:不要直接重交原题,先跑最小用例确认修复真的生效,再独立重写整题。这个习惯到模块 4 调试动态规划(DP)、模块 7 调试量化数值时,会成倍地省时间。
06 / 独立重写与复测清单
不看旧代码从空文件重写,用清单验证后再提交
独立重写检验的是「离开旧代码还能不能写对」。重写前只允许看题目页和自己的题目要求清单,不看旧代码、不看参考程序。
| 题目 | 题面示例 | 边界用例 | 错误专项用例 |
|---|---|---|---|
| P2650 找朋友 | 8 / 123 124 125 121 119 122 126 123 → 1 2 6 5 5 6 0 0 | 2 / 100 95 → 0 0;0 → 空行 | 3 / 170 170 170 → 0 0 0(判定写成 ≥ 会错);输出下标从 0 起、不加 1 |
| P3708 周末爬山 | 自拟 3 3 1 / 0 1 2 / 1 5 3 / 2 3 4 → 4 4 | 2 2 1 / 0 0 / 0 0 → 0 0;1 3 2 / 5 3 1 → 0 0 | 2 3 3 / 0 3 1 / 1 1 3 → 3 1(同高取步数短) |
| P3757 主次关联成环警告 | a b / c b → [1001,(b)];a b / b a → [1002,cycle] | a a → [1002,cycle];a b / b c → [1003,Verified] | a b / a b / b c → [1003,Verified](重复行去重);a b / c b / b a → [1001,(b)] |
| P2608 解压报文 | 题面示例(题目页给出输入与输出) | 10[x](两位数次数);abc(没有括号) | ab2[c]d(括号前后都有字母)——期望输出以交卷后的测验解析为准 |
| P3497 精准核酸检测 | 题面示例(题目页给出输入与输出) | 孤立确诊者(与任何人都没有接触) | 确诊者互相接触——期望输出以交卷后的测验解析为准 |
| P3755 BOSS 的收入 | 题面示例(题目页给出输入与输出) | 只有一层且全是整百 | 含 199 的输入;两层的输入——期望输出以交卷后的测验解析为准 |
清单里给出的输出与各课正文一致;阶段测验三题的期望输出以交卷后的测验解析为准。重写后先跑清单再提交;提交通过后把登记表的「独立重写」改成「通过」。
重写仍未通过怎么办:对比两次失败用例和代码位置——如果是同一个用例失败,说明错误原因没定位准,回到第 03 节重新分类;如果是新的用例失败,登记一条新的错题。两次都不过:登记表里写明具体问题,可以带着它先学下一模块;这道题对应的学习完成检查项等订正通过后再勾选。
07 / 迁移练习与参考答案
分类练习、找错练习、四套模板默写,以及没有错题的同学做什么
每题先自己做,再展开答案。
练习 1(分类):下面五个失败描述各属于六类中的哪一类?① P3702 题面示例通过,源点被死亡区围住时输出 1(期望 -1);② P3813 链 1—2—3 输出 2(期望 5);③ P3750 题面示例输出 A B C E D(期望 A E B C D);④ P2650 输出 2 3 7 6 6 7 0 0(期望 1 2 6 5 5 6 0 0);⑤ P3501 300×300 蛇形矿地图抛出 RecursionError。
展开练习 1 答案
① 边界(结束后没按待改造格数判 -1);② 读题(把「只有红红不能相邻」理解成「相邻不同色」);③ 建模(并列规则用了全局最小堆而不是逐轮排序);④ 输出格式(下标加了 1,题面示例证明从 0 起);⑤ 语言(递归上限没调高)。判定依据:①只有特定输入错;②③规则/方法本身错;④数字全对只差基准;⑤运行错误且逻辑无错。
练习 2(找错并写最小用例):一位同学的 P3702 在出队时才标记已访问。写出它的错误类型、一个最小用例(含期望与实际表现)和修复动作。
展开练习 2 答案
类型:复杂度(也会连带答案错误)。最小用例:YES NO NO / NO NO NO / NO NO YES(期望 2)——(1,1) 会被 (0,1)、(1,0)、(1,2)、(2,1) 四个邻居各入队一次。具体错误输出取决于写法:todo -= 1 仍留在入队处时,(1,1) 被减 4 次、最后 todo 不为 0 → 输出 -1;把减一也移到出队处时,重复条目让层数多算一轮 → 输出 3。大网格上队列膨胀还会超时。修复:在 append 的同一刻标记(本模块第 2 课 第 03 节的 callout)。复测:3×3 两天例 → 2 仍然通过。
练习 3(四套模板默写):不看资料,各写一遍并用一个断言验证——① 多源 BFS「源点入队 → 整层出队 → 入队即标记 → 走完一层计数」;② DFS 连通块「越界 → 空地 → 已访问;标记;本格 + 四方向」与回溯「选择 → 递归 → 撤销」;③ Kahn「入度 0 入队 → 减到 0 入队 → 长度判环」;④ Dijkstra「出堆 → 过期跳过 → 松弛压堆」。
展开练习 3 答案
每套模板的参考实现都在对应课的展开区:① 第 2 课 练习 4(spread_days);② 第 3 课 练习 4(count_red_black)与 P3501 完整程序;③ 第 4 课 练习 4(topo_order);④ 第 5 课 练习 4(dijkstra)。默写后与之对照,差异处就是要再练的点。
练习 4(没有错题的同学):把本模块印象最浅的一道必做题不看旧代码独立重写并用第 06 节的清单复测;然后从 P3504(第 5 课)的并查集写法出发,自学最小生成树(克鲁斯卡尔算法(Kruskal):边按权排序、并查集判环)与强连通分量入门——这部分不计入本课时长,也不是必做。
08 / 复习入口与完成条件
订正完成之后做什么
订正只针对本模块;每类错误对应的课入口如下,订正完成后按顺序进入模块 4。
| 错误类型 | 回到哪里 | 重点看什么 |
|---|---|---|
| 读题(起点、规则、方向) | 拓扑排序与依赖关系、深度优先搜索与回溯 | 第 4 课 第 03 节边方向约定表、第 3 课 第 05 节规则说明 |
| 建模(栈内容、起点集合、状态维度) | 栈、队列与单调栈、最短路:Dijkstra 与分层状态 | 第 1 课 第 07 节逐层栈表、第 5 课 第 07 节分层逐状态表 |
| 边界(-1 判定、自环、不连通) | 广度优先搜索:网格与多源扩散 | 第 04 节无法全覆盖的判定 |
| 复杂度 / 语言(标记时机、递归上限、空栈弹出) | 广度优先搜索:网格与多源扩散 | 第 03 节「标记打在入队那一刻」与补充「递归深度」 |
| 输出格式(下标基准) | 栈、队列与单调栈 | 第 05 节「输出的是下标(从 0 起)」 |
完成条件:本课算完成 = 勾选全部四条「学习完成检查」(第 3 条:2–3 道必做题从空文件独立重写;第 4 条:未通过的题全部订正);三道重做任务是复习题,判题结果不直接参与完成判定。订正不了的题写明具体问题后可以先进入下一模块,对应检查项等订正完成再勾选。错题订正完成后,先把本模块最没把握的必做题独立重写一次;还有余力,再选学并查集 / 最小生成树 / 强连通分量。仍然不加新的必做题——模块 4 的内容在后面。
09 / 练习
按顺序完成本课的任务
编程任务已通过 0/3 道
错题登记:每题一条,逐条订正
重做任务 1自主练习练习重点:本模块全部错题按六类错误原因登记,写清触发用例与错误原因;预计用时:30 分钟
完成标准:每道错题都记录了具体错误原因、触发用例和修正方法
需要时查看提示
错误原因要具体到能据此直接修改代码:「粗心」「没注意」不符合要求,「只让第一个 YES 入队,其余源点被当成普通格子」符合。分不清「建模」和「读题」时:遗漏了题目条件属于读题,方法本身选错属于建模。登记模板与填好的示例见第 05 节。
P2650 · 找朋友 · 独立重写
重做任务 2练习重点:不查看参考代码重写单调栈:核对位置基准(下标从 0 起)和判定符号,并用等高等边界用例验证;预计用时:20 分钟
完成标准:不查看模块 3 · 第 1 课 的课程页面,独立完成并通过(AC)
需要时查看提示
先口述「栈里存什么、什么时候弹、弹时结算什么」再动手;输出的是朋友的下标(从 0 起),题面示例 1 2 6 5 5 6 0 0。用第 06 节清单里的等高用例验证。
P3708 · 周末爬山 · 独立重写
重做任务 3练习重点:从空文件重写:过滤条件与「广度优先搜索结束后统一选答案」;预计用时:25 分钟
完成标准:不查看模块 3 · 第 2 课 的课程页面,独立完成并通过
需要时查看提示
先写基本写法再加约束;答案选择放在搜索之后;没有比起点更高的可达格输出 0 0。用第 06 节清单里「同高取步数短」的用例验证。
P3757 · 主次关联成环警告 · 独立重写
重做任务 4练习重点:从空文件重写:方向约定、去重、两种异常分开判;预计用时:25 分钟
完成标准:不查看模块 3 · 第 4 课 的课程页面,独立完成并通过
需要时查看提示
动笔前把「多个父节点」和「环」的判据各写一行在纸上;两种异常同时存在输出 1001。用第 06 节清单里的重复行与自环用例验证。
提交结果
提交结果说明与处理方法
- WA
答案错误
独立重写仍未通过:对比两次失败用例和代码位置,重新检查错误原因并更新登记表(第 06 节末段)
- PE
格式错误
还是格式错就把输出段单独抽出来,和样例逐字节对比;P2650 的下标从 0 起
- RE
运行错误
语言类错题:写 3 行小实验验证(递归上限、
deque空时popleft、按逗号拆)- TLE
超时
出队才标记、
list.pop(0)、每轮扫全体找入度 0、不跳过过期条目——四种写法各退化一个数量级,记成复杂度类错题- AC
通过
在登记表中将这条错题标记为已完成;所有错题完成修改、通过验证并更新状态后,本次错题订正的订正任务完成
10 / 学习完成检查
本课学习完成检查
完成本课需要:没有新的必做题,勾选全部学习完成检查即算完成。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。