0007 · 实验一:把一道题跑通

实验一:把一道题跑通

本节目标:交出一份能读入题目输入、给出答案、并用样例自动判定对错的脚本,让分层判分五层全过。

第 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。哪一层没过就回哪一步—— 按层的失败信息改,别一次改五个地方。

自查清单

最可能卡在哪

坑一:写完直接扔进大测试,样例错在哪都看不出。 表现是:代码刚写完就 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 倍)

卡壳顺序

按这五步来,别一上来就问:

  1. 读错误信息。 判分脚本会把它跑出来的报错最后几行贴出来,并指到脚本里的那一行;先按那一行看过去(ValueError 后面那句话往往直接说明了原因)。
  2. 自己跑一遍。 cat sample_a1.in | py solve_a.py 单独跑一次,看你到底打印了什么:打出 0 就是 count_distinct 还没接上,一行都不打印就是 solve() 没接上。
  3. 看 FAIL 行。 每一行 FAIL 后面都写了期望、实际,以及「这一层指向哪一段」;顺着那一句改。
  4. 再对线索。 折行组错 → 输入解析层;大数据超时 → 容器复杂度;探针报 PASS → 自测脚本没真比对;预计值偏小 → 操作次数按平方级算。
  5. 对照参考解。 五层都过了,再打开 lab/solutions/ 比一比——重点比「分段的方式」和「那一行决定快慢的代码」,不是比谁的代码短。

做完之后

这份骨架就是配套项目「AI 刷题助手」最底下的一环:后面的实验(对样例、对拍、跑验证)都是在这个「读入 → 求解 → 打印 → 用样例自动判对错」的骨架上长出来的。交完之后用一句话回答:这两份脚本里,哪一行决定了题目 A 能不能跑完十万个数?

没看懂?随时问 把看不懂的段落(或报错)原样复制回会话,说一句哪里不懂就行,我只解释这一段,不打断你的进度。