01 / 本课学习路线
本课学习路线
预计约 150 分钟,具体时长根据待订正题目数量调整
02 / 订正流程与完成条件
订正不是重做一遍:四个动作,每道错题各走一次
「再做一遍」不是修复。订正的定义是:说清第一次错在哪一类、用一个最小用例复现它、改掉它、证明改对了、再不看旧代码重写一次。前三个模块的错题订正已经走过三遍,本课把同一套流程用在本模块的动态规划题上。
| 动作 | 做什么 | 产出 | 对应正文 |
|---|---|---|---|
| ① 分类 | 按第一次出错的原因归到六类之一 | 登记表的「首次错误类型」一栏 | 第 03 节 |
| ② 定位并修复 | 找到导致错误的那几行代码或那条被漏读的规则,改掉 | 登记表的「错误原因」「修复动作」 | 第 03、04 节 |
| ③ 最小验证 | 构造一个只触发该错误的输入,修复前失败、修复后通过 | 登记表的「触发用例」「最小验证」 | 第 04、05 节 |
| ④ 独立重写 | 不看旧代码从空文件重写整题,用复测清单验证后提交 | 登记表的「独立重写」 | 第 06 节 |
完成条件:本课没有新的必做题;下面的「重做任务」是复习题,它们的判题结果不直接参与完成判定。本课算完成 = 勾选全部四条「学习完成检查」,其中第 3 条要求 AI017、AI020 两道独立重写都通过判题——只重写一道、或只在登记表写明问题,都不能勾选这一条。暂时订正不了的题可以在登记表写明具体问题、先进入下一模块学习,本课保持待完成,等两道重写通过后再勾选。先用触发原错误的输入检查修改,再验证边界和题面样例;这些检查通过后,再独立重写并提交。
| 来源 | 题号 | 判题结果 | 先猜的错误类型 | 订正状态 |
|---|---|---|---|---|
| 阶段测验第 1 题 | P3396 | 答案错误(WA) / 运行错误(RE) / 未完成 / 通过(AC) | — | 未开始 |
| 阶段测验第 2 题 | P3430 | 同上 | — | 未开始 |
| 阶段测验第 3 题 | P4204 | 同上 | — | 未开始 |
| 第 1~5 课 未通过或看提示才通过的必做题 / 进阶题 | 逐题填 | 填判题结果 | — | 未开始 |
「先猜的错误类型」在第 03 节按判定信号确认后再改;AC 的题只登记曾经改过的具体问题,不进入订正流程。看提示才通过的题按「未通过」处理——提示告诉你的那一步,就是要独立重写验证的那一步。
03 / 六类错误的判定与定位
判定信号、定位步骤、修复动作、最小用例怎么构造(本模块版);先查初始化再查转移
分类不靠感觉,靠信号:先看判题结果是哪一种,再用下面的问题逐个排除。本模块的题多了「五要素」这一层——状态含义、哨兵位、遍历方向、答案位置——它们的错误几乎都落在建模与边界两类。
| 类型 | 判定信号 | 定位步骤 | 修复动作 | 最小用例怎么构造 |
|---|---|---|---|---|
| 读题 | 样例能过、某些用例 WA;对「合法区间」「单位」「相同不算」的理解与题目页不一致 | 重新写题目要求清单:合法转移的区间(10..26)、容量单位(块 / 字节)、严格还是不严格、无解输出;逐条对照代码 | 按那一条改一处 | 构造只考那一条规则的输入(案例 A:1474561 字节) |
| 建模 | 所有用例都错或大面积错;说不清 dp 的下标代表什么、转移读的是新值还是旧值 | 把五要素写在纸上,用题面示例逐格手推(第 1 课 的填表、第 4 课 的逐件表、第 5 课 的逐轮表) | 改状态含义 / 遍历方向 / 转移来源那一处 | 取比题面示例更短的输入(单物品 (2,3)、容量 4:正序 6、倒序 3) |
| 边界 | 只有特定输入错:n = 0 / 1、空串、全负、恰好等于容量、开局状态 | 按下面的五步排查顺序,先查初始化 | 补哨兵位 / 不可达标记 / 边界行列 / 缺项判断 | 就是那个最小输入本身(案例 B:dp[0]) |
| 复杂度 | 超时(TLE),且不是读入问题 | 估算最坏次数:递归不加缓存、二维表开到 2000×2000 再逐格建列表、内层循环调函数 | 递推 / 滚动数组 / 内层只做数组读写 | 构造最大规模输入计时 |
| 语言 | 运行错误(RE)或结果对不上却找不到逻辑错 | 写 3 行小实验:负下标不报错、[[0]*n]*m 别名、.strip() 吃掉空行、按空格拆逗号行 | 换成明确的写法(显式判断 i ≥ 3、列表推导建表、按 token 总数切分) | 触发该语言行为的最短输入 |
| 输出格式 | 答案错误(WA)或格式错误(PE),数字都对 | 把自己的输出与期望输出逐字节对比 | 改舍入方式 / 补零位数 / 行数 | 题面示例或中点值(案例 C:1/32) |
初始化专项:排查顺序(写代码时对照)
① n = 0 / 1 / 2 手算期望值,对照程序输出 ② 查哨兵位:dp[0] 的含义和取值(空前缀取 0、1 还是 -∞)——计数题是 1,最值题是 0 ③ 查「恰好」类问题的不可达标记(-∞ / 计数 0);起点不是 dp[0] 的变式(开局冷却)尤其要查 ④ 查边界行列是否完整覆盖(二维题第 0 行和第 0 列都要;编辑距离的 d[i][0] = i、d[0][j] = j) ⑤ 前四步都通过,再用单物品或单字符反例检查转移(背包正序 → 6;相同字符 +1 → 5)
为什么先查初始化再查转移
初始化错误和转移错误的表现常常相同(答案偏大或偏小),但初始化错误通常在最小样例上就会暴露(P3409 全 0 初始化在题面示例上就输出 0),转移错误往往要中等规模才出现(背包正序在单物品上才 6,在多数样例上碰巧正确)。先用最小样例做低成本检查,再决定是否需要逐行读转移,可以缩小排查范围。
登记时的一条硬要求:「错误原因」必须具体到能据此直接改代码——「粗心」「没注意」不符合要求,「dp[0] 写成 0,空选择这 1 种方案没登记」符合。写不出具体原因,说明还没定位到,回到定位步骤。
04 / 三个完整订正案例
失败用例 → 定位 → 修改前后对照 → 复测
三个案例都来自本模块真题里真实会出现的错误,每个都走完整流程;你的错题按同样的格式处理。
案例 A · 读题类:P3415 块数向下取整
判题结果: 答案错误(WA);文件都是 512 的整数倍时能过 失败用例: 1 个文件 1474561 字节 期望 0 实际 1474561 定位: 重写题目要求清单 → 「按块分配、一个块只能给一个文件」→ 513 字节要占 2 块;代码用了 size // 512 修复: 向上取整 (size + 511) // 512(见下方前后对照) 复测: 1474561 → 0 ✓ 1474560 + 1 → 1474560 ✓ 300、300、1473536、1000 → 1474536 ✓
案例 A 修改前(片段)
Python# P3415 修改前:块数向下取整 —— 1474561 字节算成 2880 块,被当成刚好装得下
blocks = size // 512案例 A 修改后(片段)
Python# P3415 修改后:题面「按块分配、一个块只能给一个文件」→ 向上取整,1474561 字节要 2881 块,装不下
blocks = (size + 511) // 512只改了换算那一行;完整程序在第 4 课 第 08 节展开区。修改前后各跑一次三个复测用例,修改前第一个用例失败、修改后全部通过——这才是「修复生效」。
案例 B · 边界类:P3409 计数版忘了 dp[0] = 1
判题结果: 答案错误(WA);所有输入都输出 0 失败用例: 题面示例 5,4,2,3,2,4,9 / 10 期望 4 实际 0 定位: 排查顺序第 ② 步:dp[0] 的含义是「恰好 0 人」——一个团都不上就是 1 种方案,全 0 初始化把这个起点丢了 修复: 循环前加 dp[0] = 1(见下方前后对照) 复测: 5,4,2,3,2,4,9 / 10 → 4 ✓ 1,2,3,4 / 3 → 2 ✓ 5,5 / 3 → 0 ✓
案例 B 修改前(片段)
Python# P3409 修改前:dp 全 0 —— 「一个团都不上」这种方案没有被算作 1 种,全表永远是 0
dp = [0] * (cap + 1)
for p in people:
for c in range(cap, p - 1, -1):
dp[c] += dp[c - p]案例 B 修改后(片段)
Python# P3409 修改后:dp[0] = 1 是所有方案计数的起点(空选择恰好 0 人)
dp = [0] * (cap + 1)
dp[0] = 1
for p in people:
for c in range(cap, p - 1, -1):
dp[c] += dp[c - p]边界类错误的定位工具是排查顺序:最小输入 + 哨兵位含义。最值版背包全 0 初始化是对的,计数版必须 dp[0] = 1——同一结构、问法一变,初始化跟着变(第 4 课 第 07 节)。
案例 C · 输出格式类:AI020 浮点格式化在中点上舍错
判题结果: 答案错误(WA);大多数用例都过,只有 32 个 token 错 1 个那组不过
失败用例: d = 1、n = 32 期望 0.0313 实际 0.0312
定位: 逐字节对比:距离对、小数最后一位差 1;写 3 行小实验 f"{1/32:.4f}" → 0.0312(0.03125 精确在中点,按五成双)
修复: 整数舍入 (20000·d + n) // (2n),再拼 4 位补零(见下方前后对照)
复测: 1/32 → 0.0313 ✓ 1/3 → 0.3333 ✓ 3/0 → 3.0000 ✓ 0/0 → 0.0000 ✓案例 C 修改前(片段)
Python# AI020 修改前:浮点格式化 —— 1/32 = 0.03125 正好在中点,按「五成双」打印成 0.0312
print(f"{d / n:.4f}")案例 C 修改后(片段)
Python# AI020 修改后:整数舍入 (20000·d + n) // (2n),再拼成 4 位小数;n = 0 按题目规定 CER = d
r = (20000 * d + n) // (2 * n) if n > 0 else d * 10000
print(f"{r // 10000}.{r % 10000:04d}")输出格式类错误的最小用例是「能暴露格式差异的那个值」:一个 1/32 就够。完整程序在第 5 课 第 08 节展开区。
05 / 错题登记与最小验证
登记模板、填好的示例、最小用例的三个要求
登记表是订正的账本:每道错题一条,订正完把「独立重写」一栏改成「通过」。下面先给空模板,再给案例 B 填好的样子。
错题登记模板(每题一条)
Python# 错题登记(每题一条;订正完把「独立重写」改成「通过」)
# 题号:
# 来源: 阶段测验第_题 / 课_ 必做 / 课_ 进阶
# 判题结果: WA / RE / TLE / PE
# 首次错误类型: 读题 / 建模 / 边界 / 复杂度 / 语言 / 输出格式
# 触发用例: 输入=____ 期望=____ 实际=____
# 错误原因: (具体到能据此直接改代码)
# 修复动作:
# 最小验证: (至少三条:触发错误的、边界另一侧的、题面示例)
# 独立重写: 未做 / 通过 / 再错(类型=__)复制进你的错题本,每订正一项填一条;如果暂时无法准确描述错误原因,请结合失败用例和代码位置进一步检查,再回到六类错误原因重新分类。
填好的示例(案例 B)
Python# 题号: P3409
# 来源: 第 4 课 进阶
# 判题结果: WA
# 首次错误类型: 边界
# 触发用例: 输入=5,4,2,3,2,4,9 / 10 期望=4 实际=0
# 错误原因: dp 全 0 初始化,「什么都不选、恰好 0 人」这 1 种方案没登记,所有 dp[c] 都从 0 累加出 0
# 修复动作: 循环前加 dp[0] = 1(最值版全 0 可以,计数版起点必须是 1)
# 最小验证: 5,5 / 3 → 0 ✓(无解仍是 0) 1,2,3,4 / 3 → 2 ✓ 5,4,2,3,2,4,9 / 10 → 4 ✓
# 独立重写: 通过「触发用例」写明期望与实际;「错误原因」写到能直接改代码;「最小验证」至少三条:触发错误的、边界另一侧的、题面示例。
| 要求 | 含义 | 反例 |
|---|---|---|
| 只触发这一类错误 | 修复前失败、修复后通过,且不涉及其它规则 | 拿 300 件物品验证「容量方向」 |
| 尽量短 | 手算能在 1 分钟内得到期望输出 | 用 2000×2000 的序列验证「边界初始化」 |
| 带期望输出 | 登记时写清期望与实际 | 只写「输入 1 4 / 2 3」不写期望 |
每订正一项做一次最小验证:不要直接重交原题,先跑最小用例确认修复真的生效,再独立重写整题。本模块每课第 09 节的错误表都是现成的最小用例来源。
06 / 独立重写与复测清单
不看旧代码从空文件重写,用清单验证后再提交
独立重写检验的是「离开旧代码还能不能写对」。重写前只允许看题目页和自己的五要素注释,不看旧代码、不看参考程序。
| 题目 | 基础用例 | 边界用例 | 错误专项用例 |
|---|---|---|---|
| AI017 显存装箱 | 4 10 / 3 4 / 4 5 / 5 6 / 2 3 → 13;3 5 / 0 7 / 6 9 / 5 4 → 11 | 3 0 / 0 5 / 0 3 / 2 9 → 8(预算 0 仍部署 w=0) | 1 4 / 2 3 → 3(正序会得 6);3 128 / 128 999 / 129 10000 / 64 500 → 999(下界漏 w 会得 500) |
| AI020 语音识别 CER | 5 5 / a b c d e / a x c e f → 3、0.6000;3 0 / w1 w2 w3 / 空 → 3、1.0000 | 0 3 / 空 / x y z → 3、3.0000;0 0 / 空 / 空 → 0、0.0000 | 32 个 token 错 1 个 → 0.0313(浮点会得 0.0312);3 4 / a1 a2 a3 / b1 b2 b3 b4 → 1.3333(分母用 m 会得 1.0000) |
| P3399 猴子爬山 | 3 → 2;5 → 4 | 0 → 1;1 → 1 | 1 → 1(去掉 i ≥ 3 的判断,仍累加 f[i-3],会得 2) |
| P3403 跳格子 | 2 7 9 3 1 → 12 | 5 → 5 | 2 1 1 2 → 4(每一轮都令 dp[i] = take,漏掉 skip 分支,会得 3) |
| P3398 园区参观路径 | 3 3 / 0 0 0 / 0 0 0 / 0 0 0 → 6 | 2 2 / 0 1 / 1 0 → 0 | 3 3 / 0 0 0 / 0 1 0 / 0 0 0 → 2(忽略障碍判断、把所有格子都按上方加左方转移,会得 6) |
表中 / 表示换行。先用这些已学题的用例检查常见错误,再验证自己的错题。阶段测验的专属复测用例在交卷后的测验解析中查看。重写后先跑用例再提交;提交通过后把登记表的「独立重写」改成「通过」。
独立重写的自测模板
Python# 请使用以下函数接口独立重写一道已通过的题,并补充题目样例和边界样例断言
def solve(inp: str) -> str:
# 待完成:先在不参考旧代码和题解的情况下独立实现;完成后再对照检查
return ""
# 先填写四个字段(原样粘贴题目样例;边界样例自行构造并手算期望值),再运行
SAMPLE_IN = "" # 题目样例输入
SAMPLE_OUT = "" # 题目样例输出
EDGE_IN = "" # 自行构造一个 n=1 的最小边界输入
EDGE_OUT = "" # 手算出来的期望输出
assert SAMPLE_IN and SAMPLE_OUT and EDGE_IN and EDGE_OUT, "四个样例字段尚未填写,请先填写再运行"
assert solve(SAMPLE_IN).strip() == SAMPLE_OUT.strip(), "题目样例未通过"
assert solve(EDGE_IN).strip() == EDGE_OUT.strip(), "最小边界样例未通过"
print("代码自测通过")四个样例字段填好再运行:题目样例原样粘贴,最小边界样例自己构造并手算期望值。独立重写用于检查能否在不参考讲解的情况下完成状态定义、转移和边界处理,查看旧代码就失去了检验作用。
重写仍未通过怎么办:对比两次失败用例和代码位置——如果是同一个用例失败,说明错误原因没定位准,回到第 03 节重新分类;如果是新的用例失败,登记一条新的错题。两次都不过:登记表里写明具体问题,可以带着它先学下一模块;但 AI017、AI020 两道独立重写通过前,本课保持待完成。
07 / 迁移练习与参考答案
分类练习、找错练习、五要素默写,以及没有错题的同学做什么
每题先自己做,再展开答案。
练习 1(分类):下面五个失败描述各属于六类中的哪一类?① AI017 预算 4、单件 (2,3) 输出 6(期望 3);② AI019 输入 06 输出 1(期望 0);③ P3397 题面示例输出 6(期望 9),第 0 行第 0 列全是 0;④ P3394 输入 1,-5,-6,4,3,6,-2 时抛出 ValueError(期望 11);⑤ AI020 某组输出 0.313(期望 0.0313)。
展开练习 1 答案
① 建模(容量正序,同一物品被重复选取——遍历方向属于五要素);② 读题(两位成码的下界 10 没写);③ 边界(边界行列没初始化);④ 语言(逗号行按空格拆);⑤ 输出格式(小数部分没补零)。判定依据:①方法本身错、多数样例碰巧对;②只有含 0 的用例错;③只有依赖边界的格子错;④运行错误且逻辑无错;⑤数字全对只差格式。
练习 2(找错并写最小用例):一位同学的 AI025 只把显存维倒序、算力维正序。写出它的错误类型、一个最小用例(含期望与实际表现)和修复动作。
展开练习 2 答案
类型:建模(遍历方向)。最小用例:W = C = 2、单件 (0,1,1)——显存需求 0 时 dp[w-0] 就是同一行,算力维正序读到的是本轮刚写的新值,这件物品被计入两次,输出 2,期望 1。修复:两层容量循环都倒序 range(X, x-1, -1)。复测:题面示例 1 → 100、示例 2 → 16 仍通过。
练习 3(五要素默写):不看资料,为下面三题各写五句话注释(状态、转移、初始化、遍历顺序、答案位置),再与对应课对照:① 猴子爬山 P3399;② 0/1 背包 AI017;③ 编辑距离 AI020。
展开练习 3 答案
对照位置:① 第 1 课 第 04 节;② 第 4 课 第 04 节(注意遍历顺序是「物品外层任意、容量内层倒序」);③ 第 5 课 第 08 节的五要素表(初始化是两行 d[i][0] = i、d[0][j] = j,答案在右下角)。默写后逐条对照,差异处就是要再练的点。
练习 4(没有错题的同学):本课的独立重写题固定是 AI017 与 AI020(第 06 节有复测清单),两道都通过才能勾选学习完成检查的第 3 条;之后再挑本模块印象最浅的一道必做题不看旧代码重写一次。然后从第 4 课 的 0/1 背包出发,自学分组背包(每组至多选一件:组外层、容量倒序、组内枚举)与区间动态规划入门(dp[l][r] 由更短的区间算出,按区间长度从小到大遍历)——这部分不计入本课时长,也不是必做。
08 / 复习入口与完成条件
订正完成之后做什么
订正只针对本模块;每类错误对应的课入口如下,订正完成后按顺序进入模块 5。
| 错误类型 | 回到哪里 | 重点看什么 |
|---|---|---|
| 读题(合法区间、单位、严格) | 线性动态规划与合法转移、0/1 背包与容量遍历顺序 | 第 2 课 第 04 节三条规则表、第 4 课 第 06 节单位换算 |
| 建模(状态含义、遍历方向、新旧值) | 网格动态规划与空间优化、0/1 背包与容量遍历顺序 | 第 3 课 第 05 节滚动数组、第 4 课 第 04 节倒序反例 |
| 边界(哨兵位、不可达、边界行列) | 动态规划基础:状态、转移与遍历顺序、编辑距离与状态机模型 | 第 1 课 第 05、07 节,第 5 课 第 04、07 节 |
| 复杂度 / 语言(递归、负下标、别名、切分) | 动态规划基础:状态、转移与遍历顺序、网格动态规划与空间优化 | 第 1 课 第 03 节调用次数表与第 07 节,第 3 课 第 02 节自测 1 |
| 输出格式(舍入、补零) | 编辑距离与状态机模型 | 第 06 节舍入对照表 |
完成条件:本课算完成 = 勾选全部四条「学习完成检查」(第 3 条:AI017、AI020 两道独立重写都通过判题);两道重做任务是复习题,判题结果不直接参与完成判定,但第 3 条的勾选以它们通过为前提。订正不了的题写明具体问题后可以先进入下一模块,本课保持待完成。错题订正完成后,先把本模块最没把握的必做题独立重写一次;还有余力,再选学分组背包 / 区间动态规划。仍然不加新的必做题——模块 5 的内容在后面。
09 / 练习
按顺序完成本课的任务
编程任务已通过 0/2 道
错题登记:每题一条,逐条订正
重做任务 1自主练习练习重点:上一课「模块 4 · 阶段测验」的三题 + 本模块未通过的题,逐题按六类错误原因登记,写清触发用例与错误原因;预计用时:30 分钟
完成标准:每道错题都记录了具体错误原因、触发用例和修正方法;能说出出现次数最多的错误类型是哪一类
需要时查看提示
错误原因要具体到能据此直接修改代码:「粗心」「没注意」不符合要求,「dp[0] 写成 0,空选择这 1 种方案没登记」符合。分不清「建模」和「边界」时:遍历方向、状态含义错属于建模,初始值、缺项、边界行列错属于边界。登记模板与填好的示例见第 05 节。
AI017 · 显存装箱 · 独立重写
重做任务 2练习重点:不查看旧代码,从空文件写出容量倒序和 w=0 的处理;预计用时:30 分钟
完成标准:独立重写通过判题,并至少用一个边界用例验证;未通过时记录错误位置和原因后继续订正
需要时查看提示
写完先运行 0/1 背包课(模块 4 · 第 4 课)的单物品反例:背包函数(knap)调用 knap(4, [(2,3)]) 应等于 3,再提交。复测清单在第 06 节。
AI020 · 语音识别字符错误率 · 独立重写
重做任务 3练习重点:不查看旧代码,从空文件写出词元(token)切分、边界处理和整数舍入;预计用时:30 分钟
完成标准:独立完成并通过判题,并记录本次验证过的边界条件;未通过时记录错误原因后继续订正
需要时查看提示
先写出边界初始化的两行和舍入公式,再写主循环。用第 06 节清单里 1/32 那组验证舍入。
提交结果
提交结果说明与处理方法
- WA
答案错误
独立重写仍未通过:按第 03 节排查顺序先最小样例,再哨兵位、不可达标记、边界行列,最后用反例检查转移;对比两次失败用例并更新登记表
- PE
格式错误
还是格式错就把输出段单独抽出来,和样例逐字节对比;AI020 第二行要 4 位补零
- RE
运行错误
语言类错题:写 3 行小实验验证(负下标、二维表别名、
.strip()吃空行、按逗号拆)- TLE
超时
递归不加缓存、二维表逐格建列表、内层循环调函数——记成复杂度类错题;改用递推 / 滚动数组
- AC
通过
在登记表中将这条错题标记为已完成,并写一句本次与上次错误的差别;所有错题完成修改、通过验证并更新状态后,本次错题订正的订正任务完成
10 / 学习完成检查
本课学习完成检查
完成本课需要:没有新的必做题,勾选全部学习完成检查即算完成。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。