实验一:把一道题跑通
本节目标:交出一份能读入题目输入、给出答案、并用样例自动判定对错的脚本,让分层判分五层全过。
第 6 课结束时,你手上有几类零件:循环与 range、函数与返回值、推导式与常用内置、列表/集合/字典之间的选择,以及「先读完、再算」的输入解析规矩。这次实验不讲新语法—— 它要你把零件装成一样能跑的东西:一份能读入题目输入、把答案打印出来,而且自己就能判定对错的脚本。
这一步为什么不能跳过去:零件不装成机器,就不知道自己哪一块是空的。样例跑出错误答案时,是输入读错了、解析解析错了,还是求解算错了?这三件事在没有分段、没有自测脚本的代码里是糊在一起的,只能靠到处 print 猜。这次要交的正是把它们分开的东西—— 两个 solve 脚本、一组样例文件、一份你自己的自测脚本,外加一次规模对照。
任务、断言与判分脚本在 lab/0007-lab-01-run-one-problem/;题目材料是 problems.md;四个任务都做完、判分全过之后,再对照 lab/solutions/。
这次要做出什么
四样产物,都在 lab 目录里。文件名与函数名是判分接口,别改;函数体里怎么写随你。
| 产物 | 长什么样 | 怎么算做出来了 |
|---|---|---|
solve_a.py | 三段:一次读完输入 → 解析成 (n, a) → 求解并打印 | 四组样例全对,而且换行被改过的那一组也给出同一个答案 |
solve_b.py | 五个函数:读入、解析成 n 行 m 列的矩阵、每行最大值、最多出现的分数、打印 | 两组样例逐字一致,300×100 的数据 2 秒内跑完 |
selfcheck.py | 跑样例 → 与 .out 逐字比 → 每组一行 样例 A1 PASS,最后一行 自测通过 4/4 组 | 判分把 solve_a.py 换成故意写错的解以后,它会报 FAIL |
scale_check.py | 同一份数据上量「列表查重」与「集合查重」,再打印放大 10 倍的预计耗时 | 两行秒数差 20 倍以上;预计值超过 1 秒时限 |
给好的东西(别改):题面 problems.md、判分脚本 run_tests.py、四组样例 sample_*.in / sample_*.out、判分数据 tests/、故意写错的解 probe_wrong_a.py。留白版原样跑是不通过的:solve_a.py 打出 0、solve_b.py 打出 0 0、 selfcheck.py 一组都不报、scale_check.py 两行的秒数都小到 0.000000(函数还空着,根本没量到东西)。那是起点,不是 bug。
两道题
题目材料一共两道,输入格式不一样——这正是这次要练的地方。完整题面(含样例原文)在 problems.md。
题目 A · 不一样的数(样例是 sample_a1.in 与 sample_a2.in):
【输入格式】
第一行一个整数 n;接下来给定 n 个整数,数与数之间用空白(空格或换行)隔开。
题面只约定这 n 个整数的个数与先后顺序,不约定折行位置。
【输出格式】
一行一个整数:这 n 个数里不同数值的个数。
【规模】1 ≤ n ≤ 2×10^5,1 ≤ a_i ≤ 10^9;时限 1 秒。
题目 B · 成绩单(样例是 sample_b1.in 与 sample_b2.in):
【输入格式】
第一行两个整数 n 与 m;接下来是 n×m 个整数(每个学生的 m 门课成绩)。
按题面排版是 n 行、每行 m 个,但题面只约定这些整数的个数与先后顺序,不约定换行位置。
【输出格式】
第一行 n 个整数:每个学生的最高分,数与数之间用一个空格隔开。
第二行两个整数:出现次数最多的分数、它出现的次数;并列时取较小的那个分数。
【规模】1 ≤ n, m ≤ 1000,n×m ≤ 10^5,0 ≤ 成绩 ≤ 100;时限 1 秒。
两道题的规模都写得很大(2 \times 10^5、10^5),而样例只有几行——这不是为难你,是第 2 课那条规律的应用场合:样例永远跑得飞快,能不能过由数据规模决定。
怎么算过
算不算过,看下面这些跑得出来的东西,不看「我觉得我懂了」。每一条都来自前六课里被这次实验验收的那一课。
| 这条来自哪一课 | 做到什么算过 | 怎么自查 |
|---|---|---|
| 0006 · 读完输入再动手 | 两个 solve 脚本里标准输入只被读一次;解析交出的数据结构与题面一致;换行被改过的 tests/a3.in、tests/b3.in 与样例给出同一个答案 | 把 tests/a3.in 接到 solve_a.py 上,打印 4,与 sample_a2.out 一致 |
| 0004 · 把一段代码变成函数 | 按给定函数名分段,每个函数只做一件事:拿什么进来、交回什么出去;print 只出现在 solve 里 | 指着每个函数,能用一句话说出它的输入与输出 |
| 0005 · 推导式与常用内置工具 | 矩阵按 m 个一段切开、每行取 max、在「次数最多且分数最小」上取第一条——用推导式与内置函数收尾,而不是堆循环 | solve_b.py 里看得到至少一处推导式或内置函数的收尾写法 |
| 0003 · 列表与字典的隐藏价格 | 查重、计数用集合或字典;scale_check.py 量出列表比集合慢 20 倍以上 | 跑 py scale_check.py:两行「不同元素」一样,秒数差两个数量级 |
| 0002 · 按数据规模估算时间 | 说得出「十万个数、1 秒时限,我的循环要跑多少次」,并算出规模放大 10 倍后的耗时 | scale_check.py 第三行的预计值超过 1 秒时限,且接近实测的 100 倍 |
| 本次实验自己加的 | 四组样例逐字一致(不是只测一组);自测脚本抓得住错解;判分五层全过 | py run_tests.py 最后一行是「五层都过了」,退出码 0 |
怎么跑
先 cd 进 lab 目录,再按下面的顺序来。cat 在 PowerShell 里就是 Get-Content; CMD 里读文件写 py solve_a.py < sample_a1.in。
cd lab/0007-lab-01-run-one-problem
cat sample_a1.in | py solve_a.py # 先手动跑一组样例,看它到底打印了什么
py selfcheck.py # 你自己的自测脚本:四组样例逐组 PASS / FAIL
py scale_check.py # 任务 4 的耗时对照(要跑一秒左右)
py run_tests.py # 判分:五层一起判,逐条给 PASS / FAIL
判分为什么分五层:每一层的失败指向不同的原因,别只拿到一个笼统的「不通过」。第 1、2 层没过时,第 3–5 层会直接跳过——样例都没过,大数据组的失败信息没有参考价值。
| 层 | 跑什么 | 没过通常说明 |
|---|---|---|
| 1 · 样例 | 题面给的四组样例 | 读入 / 解析 / 求解 / 打印,哪一段还没接上 |
| 2 · 折行组 | 同一份数、换行换了排版的两组 | 输入解析层:用 input() 逐行读,读的位置与数据的行绑死了 |
| 3 · 大数据 | 题目 A 十万个数、题目 B 300×100,限时 2 秒 | 复杂度:容器选错(列表查重是 O(n^2)),不是「写错了」 |
| 4 · 自测脚本 | 你的 selfcheck.py,外加一次探针 | 自测脚本没有真的比对(探针把 solve_a.py 换成错解试它) |
| 5 · 规模对照 | scale_check.py 的三行输出 | 两种容器的量级没量出来,或把「放大 10 倍」推成了 10 倍 |
留白版原样跑,看到的会是这样的东西(这正是「指名缺哪一步」的样子):
$ py run_tests.py
第 1 层 · 样例(题面给的四组,小数据)
FAIL A1 期望 3,实际 0
把「读入解析」与「求解」分开各打印一次(第 6 课的定位手法)……
第 2 层 · 换行被打乱的小数据(同一份数、另一种排版)
FAIL A3 期望 4,实际 0
第 3 层 · 大数据(题目 A 十万个数;限时 2.0 秒)
---- 跳过:先把第 1、2 层过了再看大数据
五层:0 层通过,2 层还没过,3 层跳过
退出码:五层都过是 0,还有没过的(或跳过的)是 1。
第 1 步 · 题目 A 跑通。 在 solve_a.py 里补三段:read_all() 一次读完输入、 parse_input(text) 交出 (n, a)、count_distinct(a) 交出答案。写完手动跑一组样例(cat sample_a1.in | py solve_a.py),答案是 3 就继续。
第 2 步 · 题目 B 跑通。 在 solve_b.py 里补四个函数,重点是把 n \times m 个数解析成 n 行 m 列的二维列表。样例 2 是特意造的:每个分数出现次数一样多时,要输出最小的那个分数。
第 3 步 · 写自己的自测脚本。 在 selfcheck.py 里补三个函数,把「跑脚本 → 比对 .out」这条链路写出来。写完先 py selfcheck.py 看四组是不是全 PASS——这一步做完,以后每次改代码都只要敲这一行,不用再手工比对。
第 4 步 · 量一量再判断。 在 scale_check.py 里补两种查重写法与那个预计值,跑一次看两行秒数差多少。然后回看题目 A 的规模(n 到 2 \times 10^5):你的写法跑得完吗?
第 5 步 · 交判分。 四样都写完再 py run_tests.py。哪一层没过就回哪一步—— 按层的失败信息改,别一次改五个地方。
自查清单
- 四组样例是不是都跑过了?不是只跑
sample_a1.in就认为对。 tests/a3.in、tests/b3.in这两组换行被打乱的,是不是也给出与样例相同的答案?- 标准输入是不是只读了一次(都在
read_all()里),算法函数里没有input()? - 每个函数是不是只做一件事?
print是不是只在solve()里? - 代码里有没有留下的调试
print?标准输出应当只有题面要求的那几行。 py selfcheck.py是不是四组全 PASS、最后一行是自测通过 4/4 组?- 把
solve_a.py临时换成probe_wrong_a.py的内容,selfcheck.py会不会报 FAIL?(判分就是这么试你的;试完记得换回来。) py scale_check.py的两行「不同元素」是不是一样、秒数是不是差两个数量级?py run_tests.py是不是五层全过、退出码 0?
最可能卡在哪
坑一:写完直接扔进大测试,样例错在哪都看不出。 表现是:代码刚写完就 py run_tests.py,看到第 3 层超时或答案不对,却不知道样例是哪一组错的。判分脚本对此的处理是 第 1、2 层没过就跳过第 3–5 层;正确的顺序是:先用 py selfcheck.py 把四组样例过掉,再看折行组,最后才看大数据。
坑二:只测一组样例就认为通过。 表现是:改完只跑 sample_a1.in,看到 3 就收工。这次有四组样例、两组换行不同的数据:sample_b2.in 专门用来抓「并列时取最小」这类边界(写成 max(counts, key=counts.get) 会打出 10 2,正确答案是 5 2), tests/a3.in、tests/b3.in 专门用来抓「逐行读」。
坑三:把「打印调试」当成唯一手段。 表现是:在 solve 里到处插 print 看变量,改一次重跑一遍手工命令,最后忘了删调试输出——于是四组样例全部 FAIL(评测机逐字比)。更好的做法就是把第 3 步那份 selfcheck.py 写出来:一次写好,之后每次改完跑一行,它替你比对并指出是哪一组、期望什么、实际什么。
按经验,卡住的地方通常是这几个(按出现频率排):
| 现象或报错 | 一般缺哪一步 |
|---|---|
tests/a3.in 打出 2(应是 4) | 用 input() 一行一行读了:读指针只能往前,换行一变就读错。改成「一次读完 → 按空白切分」 |
tests/b3.in 报 ValueError: too many values to unpack | 同一处:n, m = map(int, input().split()) 把行列数与数据的行绑在一起了 |
IndexError: list index out of range,行号指在 int(tokens[0]) | read_all() 还没真的读进来(留白版是 return ''),于是 token 表是空的 |
| 第 3 层「超过 2.0 秒还没跑完」 | 容器选错:题目 A 用列表查重是 O(n^2),十万个数要几十亿次比较;集合或字典是 O(n) |
sample_b2.in 打出 10 2 | 并列取最小没处理:写成一个键 (-次数, 分数) 再取第一条 |
| 样例打出「答案是:3」这类多余的一行 | 调试 print 没删:标准输出要只剩题面要求的那几行 |
| 第 4 层「探针……仍然报 PASS」 | 自测脚本没真的比对,或者路径写死了绝对路径(探针在副本目录里跑它) |
| 第 5 层「预计值偏小」 | 放大 10 倍规模,列表查重的比较次数涨约 100 倍(不是 10 倍) |
卡壳顺序
按这五步来,别一上来就问:
- 读错误信息。 判分脚本会把它跑出来的报错最后几行贴出来,并指到脚本里的那一行;先按那一行看过去(
ValueError后面那句话往往直接说明了原因)。 - 自己跑一遍。
cat sample_a1.in | py solve_a.py单独跑一次,看你到底打印了什么:打出0就是count_distinct还没接上,一行都不打印就是solve()没接上。 - 看 FAIL 行。 每一行 FAIL 后面都写了期望、实际,以及「这一层指向哪一段」;顺着那一句改。
- 再对线索。 折行组错 → 输入解析层;大数据超时 → 容器复杂度;探针报 PASS → 自测脚本没真比对;预计值偏小 → 操作次数按平方级算。
- 对照参考解。 五层都过了,再打开 lab/solutions/ 比一比——重点比「分段的方式」和「那一行决定快慢的代码」,不是比谁的代码短。
做完之后
这份骨架就是配套项目「AI 刷题助手」最底下的一环:后面的实验(对样例、对拍、跑验证)都是在这个「读入 → 求解 → 打印 → 用样例自动判对错」的骨架上长出来的。交完之后用一句话回答:这两份脚本里,哪一行决定了题目 A 能不能跑完十万个数?
- 题目材料 problems.md(在 lab/0007-lab-01-run-one-problem/ 里)
- lab 说明 lab/README.md
- Python 官方文档 · sys
- Python 官方文档 · subprocess
- Python 官方文档 · time