01 / 本课学习路线
本课学习路线
预计约 150 分钟,具体时长根据待订正题目数量调整
02 / 订正流程与完成条件
订正不是重做一遍:四个动作,每道错题各走一次
「再做一遍」不是修复。订正的定义是:说清第一次错在哪一类、用一个最小用例复现它、改掉它、证明改对了、再不看旧代码重写一次。模块 1 的错题订正已经走过一遍,本课把同一套流程用在本模块的题上。
| 动作 | 做什么 | 产出 | 对应正文 |
|---|---|---|---|
| ① 分类 | 按第一次出错的原因归到六类之一 | 登记表的「首次错误类型」一栏 | 第 03 节 |
| ② 定位并修复 | 找到导致错误的那几行代码或那条被漏读的规则,改掉 | 登记表的「错误原因」「修复动作」 | 第 03、04 节 |
| ③ 最小验证 | 构造一个只触发该错误的输入,修复前失败、修复后通过 | 登记表的「触发用例」「最小验证」 | 第 04、05 节 |
| ④ 独立重写 | 不看旧代码从空文件重写整题,用复测清单验证后提交 | 登记表的「独立重写」 | 第 06 节 |
完成条件:本课没有新的必做题;本课算完成 = 勾选全部四条学习完成检查(含把订正不了的题写明具体问题)。下面的「重做任务」是复习题:它们的判题结果不计入完成状态,但第 3 条检查「48~72 小时复习(P3305、P3003)已完成」要求你按计划做完这两次独立重写——距离学完那两课不足 48 小时的,按实际学习日期另排到 48~72 小时后完成,再勾选这一条。先用触发原错误的输入检查修改,再验证边界和题面样例;这些检查通过后,再独立重写并提交。
| 来源 | 题号 | 判题结果 | 先猜的错误类型 | 订正状态 |
|---|---|---|---|---|
| 阶段测验第 1 题 | P3301 | 答案错误(WA) / 超时(TLE) / 未完成 / 通过(AC) | — | 未开始 |
| 阶段测验第 2 题 | P2551 | 同上 | — | 未开始 |
| 第 1~5 课 未通过或看提示才通过的必做题 / 进阶题 | 逐题填 | 填判题结果 | — | 未开始 |
「先猜的错误类型」在第 03 节按判定信号确认后再改;AC 的题只登记曾经改过的具体问题,不进入订正流程。看提示才通过的题按「未通过」处理——提示告诉你的那一步,就是要独立重写验证的那一步。
03 / 六类错误的判定与定位
判定信号、定位步骤、修复动作、最小用例怎么构造(本模块版)
分类不靠感觉,靠信号:先看判题结果是哪一种,再用下面的问题逐个排除。本模块的题多了一层「方法选型」——排序键、堆方向、前缀约定、窗口不变量、二分上下界——它们的错误几乎都落在建模与边界两类。
| 类型 | 判定信号 | 定位步骤 | 修复动作 | 最小用例怎么构造 |
|---|---|---|---|---|
| 读题 | 样例能过、某些用例 WA;并列规则或方向与题面说明不一致 | 重新写题目要求清单:每一层排序的方向、并列时保谁、输出顺序;逐条对照代码 | 按那一条改排序键或输出重排 | 构造两个只在那一层不同的元素(案例 A:热度相同、首字母大小写不同) |
| 建模 | 所有用例都错或大面积错;说不清堆里存什么、窗口不变量是什么、check 判的是什么 | 把变量含义写在纸上,用题面示例逐步手推(第 2 课 的入堆表、第 3 课 的余数表、第 4 课 的窗口表、第 5 课 的逐轮表) | 改方向 / 顺序 / 元组符号那一处 | 取题面示例或比它更短的输入(案例 B:3 5 / 1 3 2) |
| 边界 | 只有特定输入错:全负数、k=1、k=n、单元素、答案恰在上下界 | 列出四类边界输入逐个手算 | 改初值、上下界或循环范围 | 就是那个边界输入本身(案例 C:全负数) |
| 复杂度 | 超时(TLE),且不是读入问题 | 估算最坏循环次数,与数据范围比:窗口里重新求和、二分里逐个试、堆里先全入再截断 | 换成增量维护 / 二分 / 大小为 K 的堆 | 构造最大规模输入计时 |
| 语言 | 运行错误(RE)或结果对不上却找不到逻辑错 | 写 3 行小实验验证可疑的语言行为:负下标、heapq 只有小顶堆、整数除法方向、sort 稳定性 | 换成明确的写法(取负入堆、上取整公式、显式键) | 触发该语言行为的最短输入(如 nums[-1]) |
| 输出格式 | 答案错误(WA)或格式错误(PE),数字都对 | 把自己的输出与期望输出逐字节对比 | 改分隔符、行数、顺序(堆内顺序不是答案顺序) | 题面示例 |
登记时的一条硬要求:「错误原因」必须具体到能据此直接改代码——「粗心」「没注意」不符合要求,「只判断 pre % m == 0,中间段配不上对」符合。写不出具体原因,说明还没定位到,回到定位步骤。
04 / 三个完整订正案例
失败用例 → 定位 → 修改前后对照 → 复测
三个案例都来自本模块第 1、3、4 课 必做题里真实会出现的错误,每个都走完整流程;你的错题按同样的格式处理。
案例 A · 读题类:P2556 并列时没把名字转小写
判题结果: 答案错误(WA);并列项首字母大小写一致的用例都能过 失败用例: 3 / 1 2 3 4 5 / alpha 1 2 3 4 5 / Beta 1 2 3 4 5 / gamma 5 4 3 2 1 期望 alpha / Beta / gamma 实际 Beta / alpha / gamma 定位: 重写题目要求清单 → 「热度相等按名字转全小写后的字典序」,比较用的是小写形式;我的键直接比原名字,大写 B 的编码 66 小于小写 a 的 97,Beta 被排到了前面 修复: 并列那一层改成 p[0].lower(),输出仍用原名字(见下方前后对照) 复测: 上面的用例 → alpha / Beta / gamma ✓ 同一组输入把 Beta 改成 beta → alpha / beta / gamma ✓(大小写一致,两种写法结果相同) 2 / 1 1 1 1 1 / Zed 9 9 9 9 9 / alpha 1 1 1 1 1 → Zed / alpha ✓(不并列,只看热度)
案例 A 修改前(片段)
Python# P2556 修改前:并列时直接比较原名字——大写 B 的编码 66 小于小写 a 的 97
projects.sort(key=lambda p: (-p[1], p[0]))
print("\n".join(name for name, _ in projects))案例 A 修改后(片段)
Python# P2556 修改后:并列按名字转小写后的字典序(题面「热度相等按名字转全小写后的字典序」);输出仍用原名字
projects.sort(key=lambda p: (-p[1], p[0].lower()))
print("\n".join(name for name, _ in projects))只改了键那一行;完整程序在第 1 课 第 07 节展开区,三个项目的手算表在第 1 课 第 05 节。修改前后各跑一次三个复测用例,修改前第一个用例失败、修改后全部通过——这才是「修复生效」。
案例 B · 建模类:P4202 只判断 pre % m == 0(只看从头开始的段)
判题结果: 答案错误(WA);答案为 0 的用例和「从第一张牌开始就有解」的用例都能过 失败用例: 3 5 / 1 3 2 期望 1 实际 0 定位: 用第 3 课 第 05 节的余数表手推:前缀和 1、4、6 除以 5 的余数是 1、4、1——余数 1 出现了两次,中间的第 2、3 张牌 3 + 2 = 5 就是 5 的倍数;我的程序只问「余数是不是 0」,等于只看从头开始的段,方法本身少了一半 修复: 用集合记录见过的余数,再次出现即找到;空前缀的余数 0 先放进集合(见下方前后对照) 复测: 3 5 / 1 3 2 → 1 ✓ 3 7 / 1 2 3 → 0 ✓(不存在,第 3 课 第 05 节手算 2) 4 5 / 3 4 1 2 → 1 ✓(第 3 课 第 05 节手算 1)
案例 B 修改前(片段)
Python# P4202 修改前:只看「从第一张牌开始」的段——只有 pre % m == 0 时记为找到
seen = {0}
pre = 0
found = 0
for x in nums:
pre += x
r = pre % m
if r == 0:
found = 1
break
seen.add(r)案例 B 修改后(片段)
Python# P4202 修改后:两处前缀和同余 ⇒ 中间那一段的和是 m 的倍数;空前缀的余数 0 先进集合
seen = {0}
pre = 0
found = 0
for x in nums:
pre += x
r = pre % m # Python 的 % 结果恒为非负
if r in seen: # 之前出现过同样的余数 → 中间这一段的和是 m 的倍数
found = 1
break
seen.add(r)建模类错误的定位工具是「逐步手推表」:把每张牌读入后的前缀和与余数写出来,与程序的实际行为对照,第一处不一致就是要改的地方。完整程序在第 3 课 第 08 节展开区。
案例 C · 边界类:P3250 答案初值写成 0
判题结果: 答案错误(WA);题面示例 2,10,-3,-8,40,5 / 4 → 39 能过 失败用例: -5,-3,-9 / 2 期望 -8 实际 0 定位: 全负数输入错,单元素 8 / 1 也错(输出 0)→ 边界类;手算:两个窗口和是 −8、−12,最大值 −8,而初值 0 比它们都大,从没被覆盖 修复: 初值改为首窗和(见下方前后对照) 复测: -5,-3,-9 / 2 → -8 ✓ 2,10,-3,-8,40,5 / 4 → 39 ✓ 8 / 1 → 8 ✓
案例 C 修改前(片段)
Python# P3250 修改前:答案初值 0,全负数输入时 0 比任何窗口和都大
win = sum(nums[:k])
best = 0
for right in range(k, len(nums)):
win += nums[right] - nums[right - k]
best = max(best, win)
print(best)案例 C 修改后(片段)
Python# P3250 修改后:答案初值是首窗和——它是真实存在的一个窗口,不是凭空的 0
win = sum(nums[:k])
best = win
for right in range(k, len(nums)):
win += nums[right] - nums[right - k]
best = max(best, win)
print(best)边界类错误的最小用例就是那个边界输入本身:全负数、k=1、k=n、单元素、答案恰好等于上界或下界。第 4 课 第 05 节与第 5 课 第 09 节的错误表里都有这一类。
05 / 错题登记与最小验证
登记模板、填好的示例、最小用例的三个要求
登记表是订正的账本:每道错题一条,订正完把「独立重写」一栏改成「通过」。下面先给空模板,再给案例 B 填好的样子。
错题登记模板(每题一条)
Python# 错题登记(每题一条;订正完把「独立重写」改成「通过」)
# 题号:
# 来源: 阶段测验第_题 / 课_ 必做 / 课_ 进阶
# 判题结果: WA / RE / TLE / PE
# 首次错误类型: 读题 / 建模 / 边界 / 复杂度 / 语言 / 输出格式
# 触发用例: 输入=____ 期望=____ 实际=____
# 错误原因: (具体到能据此直接改代码)
# 修复动作:
# 最小验证: (至少三条:触发错误的、边界另一侧的、题面示例)
# 独立重写: 未做 / 通过 / 再错(类型=__)复制进你的错题本,每订正一项填一条;如果暂时无法准确描述错误原因,请结合失败用例和代码位置进一步检查,再回到六类错误原因重新分类。
填好的示例(案例 B)
Python# 题号: P4202
# 来源: 第 3 课 必做
# 判题结果: WA
# 首次错误类型: 建模
# 触发用例: 输入=3 5 / 1 3 2 期望=1 实际=0
# 错误原因: 只判断 pre % m == 0,等于只看从第一张牌开始的段;第 2、3 张牌 3 + 2 = 5 这种中间段配不上对
# 修复动作: 用集合记录见过的余数,r in seen 即找到;空前缀的余数 0 先放进集合
# 最小验证: 3 5 / 1 3 2 → 1 ✓ 3 7 / 1 2 3 → 0 ✓(不存在) 4 5 / 3 4 1 2 → 1 ✓(第 3 课 第 05 节手算 1)
# 独立重写: 通过「触发用例」写明期望与实际;「错误原因」写到能直接改代码;「最小验证」至少三条:触发错误的、边界另一侧的、题面示例。
| 要求 | 含义 | 反例 |
|---|---|---|
| 只触发这一类错误 | 修复前失败、修复后通过,且不涉及其它规则 | 拿一份 100 个项目的输入验证「并列时转不转小写」——里面可能同时混着读入与格式问题 |
| 尽量短 | 手算能在 1 分钟内得到期望输出 | 用 10⁵ 个数验证「答案初值」 |
| 带期望输出 | 登记时写清期望与实际 | 只写「输入 -5,-3,-9 / 2」不写期望 |
每订正一项做一次最小验证:不要直接重交原题,先跑最小用例确认修复真的生效,再独立重写整题。这个习惯到模块 4 调试动态规划(DP)、模块 7 调试量化数值时,会成倍地省时间。
06 / 独立重写与复测清单
不看旧代码从空文件重写,用清单验证后再提交
独立重写检验的是「离开旧代码还能不能写对」。重写前只允许看题目页和自己的题目要求清单,不看旧代码、不看参考程序。
| 题目 | 题面示例 | 边界用例 | 错误专项用例 |
|---|---|---|---|
| P3305 孙悟空吃蟠桃 | 自拟 3 6 7 11 / 8 → 4 | 3 6 7 11 / 3 → 0(树比小时多);30 11 23 4 20 / 5 → 30(答案 = 最大堆) | 30 11 23 4 20 / 6 → 23(p // k 写法会得错) |
| P3003 两数之和绝对值最小 | -1 -3 7 5 11 15 → -3 5 2 | 2 3 → 2 3 5(只有一对);3 -3 4 → -3 3 0(和为 0) | 1 4 -3 6 -8 → -3 4 1(不排序会错) |
| P3301 食堂供餐 | 题面示例(题目页给出输入与输出) | 3 / 100 / 1 2 3(库存远大于需求);2 / 5 / 5 5 | 3 / 10 / 10 10 10(每个单位时间都恰好取空)——期望输出以交卷后的测验解析为准 |
| P2551 拔河比赛 | 题面示例(题目页给出输入与输出) | 恰好 10 人;无末尾换行 | 同身高三人体重 75 / 70 / 60;多一行 95 70——期望输出以交卷后的测验解析为准 |
需要重算时,参照第 5 课第 05 节、第 4 课第 04 节的推演;阶段测验两题的期望输出以交卷后的测验解析为准。重写后先跑清单再提交;提交通过后把登记表的「独立重写」改成「通过」。
重写仍未通过怎么办:对比两次失败用例和代码位置——如果是同一个用例失败,说明错误原因没定位准,回到第 03 节重新分类;如果是新的用例失败,登记一条新的错题。两次都不过也不算失败,登记表里写明具体问题,带着它进下一模块。
07 / 迁移练习与参考答案
分类练习、找错练习、五套模板默写,以及没有错题的同学做什么
每题先自己做,再展开答案。
练习 1(分类):下面五个失败描述各属于六类中的哪一类?① AI023 样例通过,k=1、两候选同分时输出编号大的;② P3282 边长 c 大于行数时抛出下标越界异常;③ P4200 输入 0,0,0 输出 2(期望 0);④ P3305 对 3 6 7 11 / 8 输出 3(期望 4);⑤ P2523 输出 -4(期望 4)。
展开练习 1 答案
① 读题(并列规则「同分保编号小」漏读,元组符号写成 (得分, 编号));② 边界(c 超过 n 或 m 没有单独处理);③ 建模(找到第一个满足的位置后没有停止,输出了最后一个);④ 语言(p // k 是向下取整,上取整要写 (p + k − 1) // k);⑤ 语言 / 输出(取负入堆后输出前忘记还原负号)。判定依据:①样例能过而并列用例错 → 读题;②只有特定输入抛异常 → 边界;③所有多解输入都错 → 建模;④⑤逻辑对但语言行为没处理 → 语言。
练习 2(找错并写最小用例):一位同学的 P3303 把二分写成 mid = (lo + hi) // 2、可行时 lo = mid。写出它的错误类型、一个最小用例(含期望与实际表现)和修复动作。
展开练习 2 答案
类型:建模(求最大可行值的收缩规则与 mid 取整没有一起镜像)。最小用例:5 / 1 2 8 4 9 / 3——正确输出 3;错误程序在区间 [3, 4] 时 mid = 3、check(3) 为真走 lo = 3,区间不再缩小,永远不停(超时)。修复:mid 改为 (lo + hi + 1) // 2,不可行时 hi = mid − 1。复测:4 / 1 10 3 7 / 2 → 9 仍然通过。
练习 3(五套模板默写):不看资料,各写一遍并用一个断言验证——① 复合排序键 (−次数, 是否大写, 字母);② 大小为 K 的小顶堆淘汰规则;③ 前缀数组(pre)约定与 [l, r] 的和;④ 变长窗口:按目标确定收缩与更新的时机(最长合法:违反才收缩、收缩后更新;最短达标:达标先记录再收缩);⑤ 半开区间的 lower_bound。
展开练习 3 答案
每套模板的参考实现都在对应课的展开区:① 第 1 课 练习 4(sort_key);② 第 2 课 练习 4(topk_ids);③ 第 3 课 练习 4(build_pre / range_sum);④ 第 4 课 第 06 节(练习模板与 min_sub_len 各对应一种时机);⑤ 第 5 课 练习 4(lower_bound)。默写后与之对照,差异处就是要再练的点。
练习 4(没有错题的同学):把本模块印象最浅的一道必做题不看旧代码独立重写并用第 06 节的清单复测;然后做本模块的进阶练习里还没做的一道(P4203、P2805、P3282、P3303、P3308 任选),先写题目要求清单再动手。
08 / 复习入口与完成条件
订正完成之后做什么
订正只针对本模块;每类错误对应的课入口如下,订正完成后按顺序进入模块 3。
| 错误类型 | 回到哪里 | 重点看什么 |
|---|---|---|
| 读题(并列规则、方向) | 复合排序与并列规则 | 第 04 节方向表、题目页示例核对 |
| 建模(堆方向、窗口不变量) | 堆与 Top-K 问题、双指针与滑动窗口 | 第 2 课 第 04 节决策表、第 4 课 第 06 节收缩表 |
| 边界(初值、上下界) | 二分边界与二分答案 | 第 05 节上下界说明、第 09 节错误表 |
| 复杂度 | 前缀和与差分数组 | 第 09 节复杂度对照 |
| 语言 / 输出格式 | 标准输入输出与首次独立提交 | 第 04 节读法四种、输出四写法 |
完成条件:本课算完成 = 勾选全部四条学习完成检查(含把订正不了的题写明具体问题);两道重做任务是复习题,判题结果不计入完成状态,但第 3 条检查要求的 48~72 小时复习要按计划做完(不足间隔的按实际学习日期另排)。错题订正完成后,先把本模块最没把握的必做题独立重写一次;还有余力,再做本模块的进阶练习。仍然不加新的必做题——模块 3 的内容在后面。
09 / 练习
按顺序完成本课的任务
编程任务已通过 0/2 道
错题登记:每题一条,逐条订正
重做任务 1自主练习练习重点:本模块全部错题按六类错误原因登记,写清触发用例与错误原因;预计用时:40 分钟
完成标准:每道错题都记录了具体错误原因、触发用例和修正方法
需要时查看提示
错误原因要具体到能据此直接修改代码:「粗心」「没注意」不符合要求,「只判断 pre % m == 0,中间段配不上对」符合。登记模板与填好的示例见第 05 节。
P3305 · 蟠桃 · 独立重写
重做任务 2练习重点:先不看资料写出可行性判断函数(check)并手算两个值,再整段重写;预计用时:25 分钟
完成标准:不看模板完成,并验证收敛端点正确
需要时查看提示
先手算速度 3 要 10 小时(check(3) 为假)、速度 4 要 8 小时(check(4) 为真),再写代码。写完用第 06 节清单里「树比小时多」与「答案 = 最大堆」两个用例验证。
P3003 · 对向夹逼 · 独立重写
重做任务 3练习重点:移动方向的交换论证先口述再实现;预计用时:20 分钟
完成标准:记录答案的时机与移动顺序正确,并用示例验证
需要时查看提示
编码前先写出指针移动方向的理由:「和为负时,左指针(left)和更小的右指针(right)只会更负」,再实现;先记录再移动,用第 06 节清单里「只有一对」的用例验证。
提交结果
提交结果说明与处理方法
- WA
答案错误
独立重写仍未通过:对比两次失败用例和代码位置,重新检查错误原因并更新登记表(第 06 节末段)
- PE
格式错误
还是格式错就把输出段单独抽出来,和样例逐字节对比;堆内顺序不是答案顺序,输出前按题意重排
- RE
运行错误
语言类错题:写 3 行小实验验证(负下标、
heapq只有小顶堆、整数除法方向)- TLE
超时
窗口里重新求和、二分里逐个试、堆里先全入再截断——三种写法各退回 O(n²) 或 O(n log n),记成复杂度类错题
- AC
通过
在登记表中将这条错题标记为已完成;所有错题完成修改、通过验证并更新状态后,本次错题订正的订正任务完成
10 / 学习完成检查
本课学习完成检查
完成本课需要:没有新的必做题,勾选全部学习完成检查即算完成。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。