01 / 本课学习路线
本课学习路线
阅读与推演约 110 分钟,练习约 50 分钟,进阶练习另需约 30 分钟
02 / 学习目标与先修自测
学完本课你能做什么,以及开始前需要会什么
编辑距离的表和上一课的 P3397 是同一张网格,差别在对角边:相同字符的对角边代价从 1 变成 0,不同字符也多了一条代价 1 的对角边(替换);本课真正新的东西是输出格式(整数舍入)和状态机(状态里多一个维度)。
| 学完后能做的事 | 正文位置 | 检查方式 |
|---|---|---|
| 不看资料写出编辑距离的边界初始化并说出实际含义 | 第 04 节 | 自查第 2 条、练习 4 |
| 解释 CER 的分母为什么是参考序列长度、为什么可以大于 1 | 第 05 节 | 自查第 3 条 |
| 用整数运算实现四舍五入,并说出浮点格式化的风险 | 第 06 节 | 自查第 4 条、练习 3 |
| 说出状态机模型里不可达状态的标记方法,以及它在什么情况下才影响答案 | 第 07 节 | 自查第 5 条、练习 5 |
| 写出 token 版编辑距离 + 两行输出,通过 AI020 | 第 05、08 节 | 必做任务 1 |
先修自测:下面 5 题请先自己写答案,再展开对照。答不出的按括号里的位置补看再回来。本课假定你已完成上一课「网格动态规划与空间优化」。
| 题号 | 题目 | 补看位置 |
|---|---|---|
| 自测 1 | min(d[i-1][j-1], d[i-1][j], d[i][j-1]) + 1:三个格子各在当前格的哪个方向? | 网格动态规划与空间优化第 06 节 |
| 自测 2 | (20000 * 1 + 32) // 64 是多少?它和 1/32 × 10⁴ 四舍五入有什么关系? | 数字与运算符 |
| 自测 3 | f"{r // 10000}.{r % 10000:04d}" 在 r = 313 时打印什么?去掉 :04d 呢? | 字符串 |
| 自测 4 | "3 0\nw1 w2 w3\n\n".split() 得到什么?按行切再按空格切呢? | 标准输入输出与首次独立提交 |
| 自测 5 | float("-inf") + 5 > 0 是真是假?max(float("-inf"), 0) 是多少? | 数字与运算符 |
展开先修自测答案
自测 1:左上(替换)、上方(插入一个目标序列的元素)、左方(删除一个原序列的元素)。哪个方向叫插入、哪个叫删除由「把谁变成谁」决定——第 04 节。
自测 2:20032 // 64 = 313。1/32 = 0.03125,乘 10⁴ 是 312.5,四舍五入应为 313;分子加 n、分母乘 2 就是「加 0.5 再向下取整」的整数写法。
自测 3:0.0313;去掉 :04d 会打印 0.313(少了补零),判题逐字节比对直接答案错误。
自测 4:['3', '0', 'w1', 'w2', 'w3']——空行消失了,但按 token 总数切分(前两个是 n、m,接着 n 个、再 m 个)不受影响。按行切时 .strip() 会把末尾的空行吃掉,第 3 行就不存在了(第 09 节错误表)。
自测 5:假;0。-∞ 加任何有限数仍是 -∞,永远不会在 max 里胜出——这就是「不可达状态不参与转移」的实现方式。
03 / 概念与术语
编辑操作、操作方向、CER、状态机与哨兵值
本课的两道题分别是「两个序列」和「一个序列 + 一个附加维度」的动态规划,状态定义都要多说一句话。
| 术语 | 含义 | 代码里的位置 |
|---|---|---|
| 编辑操作 | 插入、删除、替换一个元素,各算 1 次;元素相同时不需要操作 | 转移里的三个来源 + 对角判断 |
| 操作方向 | 「把识别序列变成参考序列」:删除作用在识别序列上,插入补的是参考序列的元素 | 注释第一行;决定行、列各代表谁 |
| 编辑距离 d[i][j] | 识别序列前 j 个变成参考序列前 i 个的最少操作数 | 二维表或滚动的两行 |
| CER | 编辑距离 ÷ 参考序列长度;分母不含识别序列,所以可以大于 1 | (20000 * d + n) // (2 * n) |
| 状态机 | 每轮的选择受「当前处于什么状态」限制;把限制量(冷却剩余轮数)放进状态 | dp[r],r = 0..k |
| 哨兵值 -∞ | 标记「这个状态到不了」,让它在取最大值时永远输 | NEG = float("-inf") |
补充学习(选学)CER 舍入:为什么用整数运算约 4 分钟(20000·d + n) // (2n) 在计算什么
要求:CER = d/n 四舍五入保留 4 位。等价于对 10⁴·d/n 四舍五入取整:⌊(10⁴·d + n/2) / n⌋。分子分母同乘 2 消去 n/2 的小数:(20000·d + n) // (2n)。全程整数运算,Python、C++、Java 行为完全一致。
直接用 f"{d/n:.4f}" 大多数用例也正确,但在 .00005 边界上会舍向另一边:1/32 = 0.03125 在二进制里精确可表示,格式化按「四舍六入五成双」取到 0.0312;32 个 token 错 1 个这类输入要求 0.0313。判题逐字节比对时,这就是一次答案错误(WA)。评测类题目里「输出格式也是算法的一部分」,这是典型的例子。
补充学习(选学)编辑距离在 AI 评测中的位置约 4 分钟CER、WER、序列对齐、维特比解码属于同一类问题
语音识别和光学字符识别(OCR)的评测都在计算这张表:字符错误率(CER)用字符级编辑距离,词错误率(WER)用词级编辑距离;AI020 按题目定义以词元(token)为单位统计,并沿用题目给出的名称 CER。分母都是参考(正确)序列的长度;识别结果的长度不进分母,所以 CER 可以大于 1(识别结果比参考长很多时)。
把编辑距离表的「取最小」换成「取最优得分」,再把三个来源换成状态机允许的转移边,就是维特比(Viterbi)解码——线性动态规划课(模块 4 · 第 2 课)补充学习提到的维特比解码,就是在这张表上做上述替换。实现时重点是把转移和不可达状态写对。
04 / 操作方向与填表
先把方向写进注释,再用 cat → cut 的 4×4 表建立直觉
状态 d[i][j] = 把原串前 i 个变成目标串前 j 个的最少操作数(1 基下标,i、j 是已处理的个数)。「把识别序列变成参考序列」和反过来距离值相同,但删除和插入的含义互换:AI020 的方向是前者,写代码前把这句话放在注释第一行,转移的三个来源才不会混淆。
| '' | c | u | t | |
|---|---|---|---|---|
| '' | 0 | 1 | 2 | 3 |
| c | 1 | 0 | 1 | 2 |
| a | 2 | 1 | 1 | 2 |
| t | 3 | 2 | 2 | 1 |
终点 d[3][3] = 1:把 a 替换成 u。验证关键格:d[2][2](ca 与 cu)= min(d[1][1]=0, d[1][2]=1, d[2][1]=1) + 1 = 1;d[3][3] 因 t == t 走对角取 d[2][2] = 1。第 0 行 / 第 0 列的含义:一边为空时,距离就是另一边的长度。
与 P3397 的斜边对照
P3397 里只有字符相同才有斜边,且斜边距离是 1;编辑距离里字符相同时走对角不加操作(代价 0),字符不同时也能走对角,代价 1(替换)。最小的区分例子是 A 与 B:P3397 距离 2,编辑距离 1。同一张网格、不同的边代价定义——这就是两课都强调「以题目要求为准」的原因:公式形状相似,不代表题目要求相同。把本课相同字符的对角代价改成 1,题面示例的输出会从 3 变成 5(第 09 节错误表)。
05 / AI020 语音识别 CER
三条题目要求;题面示例的完整距离表
AI020「语音识别 CER」:第一行 n m(0 ≤ n, m ≤ 2000),第二行 n 个参考 token,第三行 m 个识别 token(长度为 0 时对应行是空行);输出两行:编辑距离、CER(编辑距离 ÷ 参考长度,四舍五入保留 4 位;参考为空时 CER 数值等于距离本身)。题面示例 1:a b c d e / a x c e f → 3、0.6000;示例 2:w1 w2 w3 / 空 → 3、1.0000。
| j=0 | a | x | c | e | f | |
|---|---|---|---|---|---|---|
| i=0 | 0 | 1 | 2 | 3 | 4 | 5 |
| a | 1 | 0 | 1 | 2 | 3 | 4 |
| b | 2 | 1 | 1 | 2 | 3 | 4 |
| c | 3 | 2 | 2 | 1 | 2 | 3 |
| d | 4 | 3 | 3 | 2 | 2 | 3 |
| e | 5 | 4 | 4 | 3 | 2 | 3 ← 答案 |
终点 3:把识别序列变成参考序列——x → b 替换、插入 d、删除 f。验证两格:d[2][2](a b 与 a x):b ≠ x,min(d[1][1]=0, d[1][2]=1, d[2][1]=1) + 1 = 1;d[5][5](e 与 f 不同):min(d[4][4]=2, d[4][5]=3, d[5][4]=2) + 1 = 3。CER = 3 / 5 = 0.6000;分母是参考长度 5,不是识别长度。
| 要求 | 处理 | 对应用例 |
|---|---|---|
| 按 token 切分而不是按字符;某一行可能为空 | data = sys.stdin.read().split(),再按 n、m 数量切片:空行自然消失,下标不错位 | 0 3 / 空行 / x y z → 3、3.0000 |
| 输出两行:距离、四舍五入 4 位小数的 CER | 整数舍入 r = (20000·d + n) // (2n),打印 r // 10000 和 4 位补零的 r % 10000 | 3 3 / a b c / a b x → 1、0.3333 |
| 参考为空时 CER 数值等于距离本身 | n = 0 时 r = d × 10⁴,同样按 4 位小数格式打印 | 0 0 / 空 / 空 → 0、0.0000 |
06 / CER 的整数舍入
浮点格式化在中点上会舍向另一边;用整数运算
四舍五入到 4 位 = 把 10⁴·d/n 加 0.5 后向下取整 = (20000·d + n) // (2n)。
| d / n | 真实值 | f"{d/n:.4f}" | 整数舍入 | 题目要求 |
|---|---|---|---|---|
| 1 / 32 | 0.03125(二进制精确) | 0.0312 | 313 → 0.0313 | 0.0313 |
| 1 / 3 | 0.3333… | 0.3333 | 3333 → 0.3333 | 0.3333 |
| 2 / 3 | 0.6666… | 0.6667 | 6667 → 0.6667 | 0.6667 |
| 4 / 3 | 1.3333… | 1.3333 | 13333 → 1.3333 | 1.3333 |
| 3 / 0(参考为空) | 按题目规定 = 3 | 除零异常 | 30000 → 3.0000 | 3.0000 |
两种写法在两类值上会不同:一类是像 1/32 = 0.03125 这样正好落在中点、二进制又能精确表示的值,格式化按「五成双」舍向偶数;另一类是二进制不能精确表示的值,例如 3/800 = 0.00375 在浮点里略小于中点,格式化得到 0.0037,而题意四舍五入是 0.0038。1/64 = 0.015625 不在 4 位小数的中点上,两种写法都是 0.0156。整数写法对所有输入都与要求一致,且 Python / C++ / Java 行为相同。
07 / 状态机:冷却期任务收益
把「冷却剩余轮数」放进状态;两组示例逐轮表;哨兵值何时起作用
AI026「冷却期任务收益」:第一行 n k(1 ≤ n ≤ 200000,0 ≤ k ≤ 10),随后 n 行 a_i b_i(−10⁴..10⁴);每轮休息、接常规(+a_i)、接加急(+b_i,之后 k 轮只能休息)三选一,第 1 轮无冷却,输出最大总收益(全休息为 0)。题面示例 1:k = 1,(2,9)(6,1)(2,9)(6,1) → 18;示例 2:k = 2,(1,10)(8,0)(9,1) → 18。
| 轮 | (a, b) | dp[0](无冷却) | dp[1](还剩 1 轮冷却) |
|---|---|---|---|
| 起点 | — | 0 | −∞ |
| 1 | (2, 9) | max(休息 0, 常规 2) = 2 | 加急 0 + 9 = 9 |
| 2 | (6, 1) | max(2, 2 + 6, 冷却结束 9) = 9 | 2 + 1 = 3 |
| 3 | (2, 9) | max(9, 9 + 2, 3) = 11 | 9 + 9 = 18 |
| 4 | (6, 1) | max(11, 11 + 6, 18) = 18 | 11 + 1 = 12 |
答案 max(18, 12) = 18:第 1 轮加急 9 → 第 2 轮冷却 → 第 3 轮加急 9 → 第 4 轮冷却。最后一轮结束时处于冷却也合法,所以答案取全部状态的最大值,不能只看 dp[0]。
| 轮 | (a, b) | dp[0] | dp[1] | dp[2] |
|---|---|---|---|---|
| 起点 | — | 0 | −∞ | −∞ |
| 1 | (1, 10) | 1 | −∞ | 10 |
| 2 | (8, 0) | 1 + 8 = 9 | 10(冷却 2 → 1) | 1 + 0 = 1 |
| 3 | (9, 1) | max(9, 9 + 9, 10) = 18 | 1 | 9 + 1 = 10 |
答案 18:三轮都接常规(1 + 8 + 9)。第 1 轮的加急 10 看起来最大,但它堵住了后两轮的 8 和 9——贪心「每轮取当前最大」只得 10。
哨兵值 −∞ 在什么情况下才真正影响答案
起点只有「无冷却、收益 0」一种合法状态,其余状态写 −∞。对 AI026 的题面来说,如果把起点的冷却状态也写成 0,答案碰巧不会变——「带冷却的 0」只能一路休息,永远不比「无冷却的 0」好,会被 max 淘汰。真正出问题的是起点不是 dp[0] 的变式:例如「开局就处于 2 轮冷却」,合法答案是只做第 3 轮的 9;全 0 初始化会让 dp[0] = 0 从第 1 轮就能接任务,得到 18(第 10 节练习 5 有断言)。规则仍然是:只把真正的起点写成合法值,其余一律 −∞——不要靠「碰巧被淘汰」。
08 / 从表到程序
两道题的完整参考程序,每一步落在哪几行
先自己写完并提交一次,再展开对照。
| 要素 | AI020 | AI026 |
|---|---|---|
| ① 状态 | d[i][j](只保留 prev / cur 两行) | dp[r]:本轮结束、冷却还剩 r 轮 |
| ② 转移 | 相同:左上;不同:min(左上, 上, 左) + 1 | 冷却中:new[r-1] = dp[r];无冷却:休息 / 常规 → new[0],加急 → new[k] |
| ③ 初始化 | prev = 0..m,每行 cur[0] = i | dp[0] = 0,其余 −∞ |
| ④ 遍历顺序 | i、j 从小到大 | 逐轮 |
| ⑤ 答案位置 | prev[m] + 整数舍入两行输出 | max(dp) |
从空文件写模板:AI020 的基本结构
Python# 请补全以下编辑距离练习模板,并明确序列方向、初始化和输出格式
import sys
def solve() -> None:
data = sys.stdin.read().split()
n, m = int(data[0]), int(data[1])
ref = data[2:2 + n] # 参考序列(按 token 总数切分,不要按行切分)
hyp = data[2 + n:2 + n + m] # 识别序列
# 方向约定(先在注释里明确):把识别序列变成参考序列
# d[i][j] = 识别前 j 个 token 对齐参考前 i 个的最少编辑次数
# 待完成 1:初始化 d[i][0] = i,d[0][j] = j
# 待完成 2:转移——token 相同走对角;不同取替换、删除、插入的最小值 + 1
# 待完成 3:输出两行——编辑距离;题目定义的 CER 四舍五入保留 4 位
# CER 舍入用整数运算 (20000*d + n) // (2*n),各语言行为一致
solve()n、m 可以为 0:一个序列为空时,距离就是另一个序列的长度——这正是边界初始化的实际含义,断言里专门放了两个空串用例。
展开完整参考程序 1:AI020 语音识别 CER(先自己写完并提交一次,再展开对照)
完整程序:AI020(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split() # 按 token 总数切分:空行不会让下标错位
n, m = int(data[0]), int(data[1])
ref = data[2:2 + n] # 参考序列(n 个 token)
hyp = data[2 + n:2 + n + m] # 识别序列(m 个 token)
# ① d[i][j] = 识别序列前 j 个 token 变成参考序列前 i 个 token 的最少编辑次数;只保留上一行 prev 和本行 cur
prev = list(range(m + 1)) # ③ d[0][j] = j:参考为空,删掉 j 个识别 token
for i in range(1, n + 1): # ④ 逐行
cur = [i] + [0] * m # d[i][0] = i:识别为空,插入 i 个参考 token
a = ref[i - 1]
for j in range(1, m + 1):
if a == hyp[j - 1]:
best = prev[j - 1] # ② 相同:走对角,不加操作
else:
best = prev[j - 1] + 1 # 替换
if prev[j] + 1 < best: # 上方:插入一个参考 token
best = prev[j] + 1
if cur[j - 1] + 1 < best: # 左方:删除一个识别 token
best = cur[j - 1] + 1
cur[j] = best
prev = cur
d = prev[m] # ⑤ 右下角
print(d)
if n > 0:
r = (20000 * d + n) // (2 * n) # CER × 10⁴ 四舍五入:全程整数
else:
r = d * 10000 # 参考为空:题目规定 CER 数值等于距离本身
print(f"{r // 10000}.{r % 10000:04d}")滚动两行:prev 是上一行、cur 是本行;2000×2000 是 4×10⁶ 次转移,内层只做整数比较。自测建议:题面两组示例、第 05 节三条要求对应的用例、32 个 token 错 1 个(0.0313)、CER 大于 1 的例子(1.3333);想进一步验证,可与二维表写法对拍随机短序列。
展开完整参考程序 2:AI026 冷却期任务收益(进阶练习)
完整程序:AI026(标准输入 → 标准输出)
Pythonimport sys
data = sys.stdin.read().split()
n, k = int(data[0]), int(data[1])
NEG = float("-inf")
dp = [NEG] * (k + 1) # ① dp[r] = 本轮结束、冷却还剩 r 轮时的最大总收益;不可达 = -∞
dp[0] = 0 # ③ 第 1 轮开始前:无冷却、收益 0,是唯一合法起点
for i in range(n): # ④ 逐轮
a, b = int(data[2 + 2 * i]), int(data[3 + 2 * i])
new = [NEG] * (k + 1)
for r in range(1, k + 1): # ② 冷却中只能休息:剩 r 轮 → 剩 r-1 轮
if dp[r] > new[r - 1]:
new[r - 1] = dp[r]
if dp[0] > new[0]: # 无冷却时休息
new[0] = dp[0]
if dp[0] + a > new[0]: # 无冷却时接常规
new[0] = dp[0] + a
if dp[0] + b > new[k]: # 无冷却时接加急:之后冷却 k 轮(k = 0 时落回 new[0])
new[k] = dp[0] + b
dp = new
print(int(max(dp))) # ⑤ 最后一轮结束时任何冷却状态都合法自测建议:题面两组示例(18 / 18)、全负(0)、k = 0(11)、单轮加急(7);n = 200000、k = 10 时约 2.2×10⁶ 次转移。想进一步验证,可写「枚举每轮三种选择」的暴力对拍短输入。
09 / 边界、反例与复杂度
错误做法在具体输入上各输出什么
下面每一行都是一个具体的错误程序,给出输入、错误输出与正确输出;把参考程序改成对应写法就能复现。
| 错误做法 | 输入 | 错误输出 | 正确输出 | 判题结果 |
|---|---|---|---|---|
| AI020 相同也 +1(P3397 的斜边代价) | 题面示例 1 | 5 / 1.0000 | 3 / 0.6000 | 答案错误(WA) |
| AI020 边界不初始化(第 0 行、第 0 列全 0) | 3 0 / w1 w2 w3 / 空 | 0 / 0.0000 | 3 / 1.0000 | 答案错误(WA) |
| AI020 分母用识别序列长度 m | 3 4 / a1 a2 a3 / b1 b2 b3 b4 | 4 / 1.0000 | 4 / 1.3333 | 答案错误(WA) |
AI020 用 f"{d/n:.4f}" | 32 个 token 错 1 个 | 1 / 0.0312 | 1 / 0.0313 | 答案错误(WA) |
AI020 小数部分不补零(去掉 :04d) | 3 3 / a b c / a b x | 1 / 0.3333(碰巧对);r = 313 时打印 0.313 | 0.0313 | 答案错误(WA) |
AI020 先 .strip() 再按行切 | 3 0 / w1 w2 w3 / 空 | 抛出 IndexError(末尾空行被吃掉) | 3 / 1.0000 | 运行错误(RE) |
| AI026 贪心:每轮取 max(a, b, 0) | 题面示例 2 | 10 | 18 | 答案错误(WA) |
| AI026 冷却中也允许接常规 | 题面示例 2 | 27 | 18 | 答案错误(WA) |
| AI026 答案只看 dp[0] | 1 3 / −2 7 | 0 | 7 | 答案错误(WA) |
第五行说明:格式错误只在某些数值上暴露,示例通过不代表格式对。第八行的 27 = 10 + 8 + 9:把「冷却期内只能休息」写丢了。
| 做法 | 时间 | 空间 | 本课规模下 |
|---|---|---|---|
| 编辑距离滚动两行 | O(nm) | O(m) | 2000×2000 = 4×10⁶ 次转移 |
| 编辑距离开满二维表 | O(nm) | O(nm) | 4×10⁶ 格,Python 列表约几十 MB,建表和访问都更慢;能否通过取决于真实的内存与时间限制 |
| 状态机 dp[r] | O(n·k) | O(k) | 2×10⁵ × 11 ≈ 2.2×10⁶ |
| 枚举每轮三选一 | O(3ⁿ) | 递归深度 n | n = 20 已 3.5×10⁹,超时 |
10 / 渐进练习与参考答案
跟做 → 改一个条件 → 独立实现 → 迁移
每题先在纸上或文件里做完,再展开答案。
练习 1(跟做):按第 04 节表的格式填出 ab → ba 的 3×3 表,写出终点值和一种对应的操作序列。
展开练习 1 答案
第 0 行 0 1 2;第 1 行(a)1 1 1;第 2 行(b)2 1 2。终点 2:把 a 替换成 b、把 b 替换成 a(或删 a 再补 a)。d[1][2] = 1 是因为 a == a 走对角取 d[0][1] = 1。
练习 2(改一个条件):只允许插入和删除、不允许替换,cat → cut 的距离是多少?它和最长公共子序列有什么关系?
展开练习 2 答案
2:删 a、插 u。去掉替换后,转移只剩上方、左方 + 1 和相同时的对角,距离 = |A| + |B| − 2 × 最长公共子序列长度 = 3 + 3 − 2 × 2(公共子序列 ct)= 2。这正是上一课练习 3 里「斜边代价 0」的 P3397。
练习 3(改一个条件):d = 2、n = 3 与 d = 1、n = 32,分别用浮点格式化和整数舍入各算一次 CER,哪一组不同?为什么?
展开练习 3 答案
2/3:两种都是 0.6667(0.6666… 不在中点)。1/32:浮点格式化 0.0312、整数舍入 0.0313——0.03125 在二进制里精确可表示且正好是中点,格式化按「五成双」取偶数位。题目要求 0.0313。
练习 4(独立实现):完成「代码自测」的 edit_dist,再加两条断言:ab → ba 为 2,sunday → saturday 为 3。
展开练习 4 答案
edit_dist 的参考实现(自带断言)
Pythondef edit_dist(a: str, b: str) -> int:
n, m = len(a), len(b)
d = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
d[i][0] = i # 目标为空:删除 i 次
for j in range(1, m + 1):
d[0][j] = j # 原串为空:插入 j 次
for i in range(1, n + 1):
for j in range(1, m + 1):
if a[i - 1] == b[j - 1]:
d[i][j] = d[i - 1][j - 1] # 相同:走对角,不加操作
else:
d[i][j] = min(d[i - 1][j - 1], d[i - 1][j], d[i][j - 1]) + 1 # 替换 / 删除 / 插入
return d[n][m]
assert edit_dist("cat", "cut") == 1 # a→u 一次替换
assert edit_dist("abc", "") == 3 # 三次删除:第 0 列初始化在这里检验
assert edit_dist("", "ab") == 2 # 两次插入:第 0 行初始化
assert edit_dist("ab", "ab") == 0
assert edit_dist("ab", "ba") == 2 # 第 10 节练习 1 的表
assert edit_dist("sunday", "saturday") == 3两个空串断言专门检验第 0 行、第 0 列。
练习 5(迁移):把 AI026 写成函数 cooldown_max(k, rows, start_cool=0),用两组示例、全负、k = 0、单轮验证;再加一个「开局冷却 2 轮」的变式:示例 2 的答案变成多少?把不可达状态改成 0 初始化,变式的输出是多少?
展开练习 5 答案
cooldown_max 的参考实现(自带断言)
Pythondef cooldown_max(k, rows, start_cool=0):
NEG = float("-inf")
dp = [NEG] * (k + 1) # dp[r] = 本轮结束、冷却还剩 r 轮时的最大收益;不可达 = -∞
dp[start_cool] = 0 # 唯一合法起点:开局冷却剩 start_cool 轮(AI026 是 0)
for a, b in rows:
new = [NEG] * (k + 1)
for r in range(1, k + 1):
new[r - 1] = max(new[r - 1], dp[r]) # 冷却中只能休息
new[0] = max(new[0], dp[0], dp[0] + a) # 无冷却:休息 / 常规
new[k] = max(new[k], dp[0] + b) # 无冷却:加急,之后冷却 k 轮
dp = new
return int(max(dp))
assert cooldown_max(1, [(2, 9), (6, 1), (2, 9), (6, 1)]) == 18 # AI026 题面示例 1
assert cooldown_max(2, [(1, 10), (8, 0), (9, 1)]) == 18 # 示例 2:三轮都接常规
assert cooldown_max(1, [(-5, -2), (-1, -1), (-3, -9)]) == 0 # 全负:全部休息
assert cooldown_max(0, [(1, 5), (2, 1), (-1, 4)]) == 11 # k = 0:每轮取 max(a, b, 0)
assert cooldown_max(3, [(-2, 7)]) == 7 # 最后一轮接加急也合法
assert cooldown_max(2, [(1, 10), (8, 0), (9, 1)], start_cool=2) == 9 # 变式:开局冷却 2 轮,只能做第 3 轮变式答案 9(前两轮只能休息,第 3 轮取 max(9, 1))。把 dp = [NEG] * (k + 1); dp[start_cool] = 0 改成全 0,变式会输出 18——不可达的 dp[0] = 0 让第 1 轮就能接任务。
11 / 读题要求与复习自评
两道题的要求对照,以及完成本课之后怎么复习
提交前把下表过一遍;题目页的题面与样例是最终依据。
| 项目 | AI020 语音识别 CER | AI026 冷却期任务收益 |
|---|---|---|
| 输入 | n m;n 个 token;m 个 token(可为空行) | n k;n 行 a b |
| 输出 | 两行:距离;CER 4 位小数 | 一个整数(全休息为 0) |
| 状态 | d[i][j] 识别前 j 个 → 参考前 i 个 | dp[r] 冷却还剩 r 轮 |
| 初始化 | d[i][0] = i、d[0][j] = j | dp[0] = 0,其余 −∞ |
| 答案位置 | d[n][m];n = 0 时 CER = d | max(dp) |
| 示例 | 5 5 / a b c d e / a x c e f → 3、0.6000 | 4 1 / 2 9 / 6 1 / 2 9 / 6 1 → 18 |
需要对照解法时,展开本课第 08 节的完整参考程序;题目页另有思路与参考代码,可在题目页查看。完成条件:本课算完成 = AI020 通过判题,并勾选全部六条「学习完成检查」;六条检查是自评,勾选不改变题目的通过(AC)状态;AI026 是进阶练习,与复习题一样单独统计,不影响完成状态。复习时用三个问题自测:① 不看正文,写出编辑距离的边界初始化和三个来源各对应哪种操作;② 不看表格,重算 1/32 的两种舍入结果;③ 说出 AI026 状态定义里 r 的含义,以及答案为什么取 max(dp) 而不是 dp[0]。答不出哪一条,就回到对应的节重读,再做第 10 节对应的练习。
12 / 练习
按顺序完成本课的任务
必做题已通过 0/1 道;进阶练习已通过 0/1 道
字符版编辑距离练习(edit_dist)
代码自测自主练习练习重点:三向转移 + 边界初始化,四个断言含两个空串;预计用时:15 分钟
完成标准:空串用例全部正确,说明边界初始化没有遗漏
需要时查看提示
先填满 d[i][0] = i、d[0][j] = j,再写双层循环。相同时走对角(不加 1),不同时取三者最小值加 1。参考实现在第 10 节练习 4 的展开区。
自测代码(复制到你的代码文件中运行,检查输出是否一致)
def edit_dist(a: str, b: str) -> int:
# 你来写:把 a 变成 b 的最少插入、删除、替换次数
...
assert edit_dist("cat", "cut") == 1 # a→u 一次替换
assert edit_dist("abc", "") == 3 # 三次删除:第 0 列初始化在这里检验
assert edit_dist("", "ab") == 2 # 两次插入:第 0 行初始化
assert edit_dist("ab", "ab") == 0AI020 · 语音识别字符错误率(CER)
必做任务 1练习重点:token 版编辑距离 + 两行输出 + 整数舍入;预计用时:35 分钟
完成标准:能说出三条题目要求:token 切分、CER 的分母、参考为空时的规定
需要时查看提示
读入按 token 总数切分(输入数组(data)的切片 data[2:2+n] 与 data[2+n:2+n+m]),可以避开空行问题。n = 0 时按题目规定 CER 数值等于距离本身;n > 0 时用 (20000*d + n) // (2*n) 舍入,拼成「整数部分.四位小数」输出。n、m 可达 2000,O(nm) 是 4×10⁶ 次转移,用只保留两行的滚动数组把内存控制在 O(m)。第 05 节有示例的完整表。
AI026 · 冷却期任务收益
进阶练习 1进阶练习练习重点:把冷却剩余轮数加入状态定义,不可达状态标 -∞;预计用时:30 分钟
完成标准:能画出状态机的节点和边,并指出哪些组合不可达
需要时查看提示
dp[r] = 本轮结束、冷却剩余 r 轮的最大收益(r=0 表示无冷却)。冷却中只能休息(r → r−1);常规只在 r=0 时可接;加急只在 r=0 时可接,接完 r=k。收益可能为负:「不接亏损任务」由取最大值与休息分支自然覆盖,全部休息时答案为 0;答案取全部状态的最大值。n 可达 2×10⁵、k ≤ 10,O(n·k) 约 2.2×10⁶ 次转移。第 07 节有两组示例的逐轮表。
提交结果
提交结果说明与处理方法
- WA
答案错误
四查:边界初始化(空串用例)、三向转移漏了插入项、CER 分母用错序列、浮点格式化在中点上舍错。第 09 节的表给出了每种错误的具体输出。
- PE
格式错误
两行输出:第一行是整数距离,第二行恰好 4 位小数(1 也要写成 1.0000,313 要写成 0.0313)。
- RE
运行错误
先
.strip()再按行切分会把末尾空行吃掉,改为按 token 总数切分。- TLE
超时
2000×2000 开满二维表在 Python 里偏慢,改为只保留两行的滚动数组;AI026 不要把 k 的循环写成扫描全部历史。
- AC
通过
说明方向反过来(参考变成识别)时,代码里要互换的是哪两个变量。如果暂时无法说明,请根据状态定义核对插入和删除分别对应哪个序列。
13 / 学习完成检查
本课学习完成检查
完成本课需要:必做题全部通过,并勾选本课的全部学习完成检查;进阶练习、基础加练与复习题单独统计,不影响完成状态。登录后,勾选记录会保存到账号,并更新课程总览的完成状态。