ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PTA在线判题作息表:测试点推进、止损线与复盘节奏

PTA在线判题作息表:测试点推进、止损线与复盘节奏

PTA 这类在线判题平台最不缺的就是题,缺的是节奏。很多人打开题目集的第一反应是往下翻,翻十分钟挑一道看着顺眼的,写二十分钟卡住,退出来换个题目集接着翻,一天下来提交记录里躺着三四条“部分正确”。我带过几批做上机训练的人之后,最大的感受是:刷题这件事的分水岭既不在智商,也不在刷了多少道,而在于有没有一张把时间钉死的作息表。“PTA 老板的作息表”这个说法,原本指的是带队的那位负责人在群里发的一张日程——几点到几点干什么、每段产出什么、什么情况下必须停手。它解决的不是“题不会做”的问题,是“一天过去了不知道自己干了什么”的问题。下面我把这张表的骨架、每一段的设计理由、以及我照着跑下来踩过的坑,完整拆一遍。刚接触在线判题的新手可以照着抄时间框架,已经刷了一段时间但卡在瓶颈的人,重点看第二、三部分的配比逻辑和第五部分的排查路径。

1. 这张作息表为什么长这个样子

1.1 先认清判题机制,才谈得上排时间

排作息表之前必须先搞清楚你面对的是一个什么样的反馈系统,因为它决定了你每一段时间该换来什么。PTA 的判题是“测试点制”:一道题背后藏着若干组数据,每组独立跑,跑过给分、跑挂不给分,最后汇总成一个结果码。这意味着你看到的不是“对/错”两个状态,而是一条连续的分数曲线——60 分、80 分、95 分都是常态。这个特性直接决定了时间安排:你不能用“做完一道题”作为时间单位,只能用“推进一个测试点”作为时间单位。

我在排表的时候把这件事想得很透。一道 25 分的综合题,从 0 分推到 20 分可能要一个小时,从 20 分推到 25 分可能要再一个小时,而最后那 5 分往往是边界数据、特殊输入、精度处理。如果不提前划分,人很容易陷在最后 5 分里出不来,一整天的计划全废。所以作息表里每段任务都必须写明“这段的验收标准是什么”,而不是“这段做几道题”。

还有一点容易被忽略:PTA 的时间限制通常压得很紧,多数题在几百毫秒到一秒之间。这不是为了刁难人,而是逼你在写代码的时候就考虑复杂度。排表的时候我把“复杂度估算”单独抽出来做了一段固定动作,原因就在这里——判题机不会告诉你哪里慢,它只告诉你超时,你得自己在动手前就把量级算清楚。n 给到 10 万,你写个双层循环就是 100 亿次操作,必然超时,这个判断应该在读题阶段完成,而不是提交之后对着“运行超时”发呆。

1.2 三个硬约束决定了时间怎么切

作息表不是拍脑袋排的,它背后有三个绕不开的约束,我把它们摆在一起看,时间切分自然就出来了。

第一个约束是注意力时长。高强度推导类任务,绝大多数人撐不过 90 分钟就开始出现“读了三遍还没读懂题”的状态。所以表里所有攻坚段都卡在 75 到 90 分钟,到点必须换类型,哪怕题没做完。这一条我一开始不服,觉得自己能连着写三小时,实测两周后发现,第三个小时写出来的代码,返工率是前两个小时的将近三倍,纯属负收益。

第二个约束是反馈周期。在线判题的反馈是秒级的,这既是好事也是陷阱。好事是你能快速验证想法;陷阱是你会不自觉地“边写边交”,把一道题拆成十几次提交,每次改动一点点,靠判题机替你调试。这种做法短期爽,长期毁手感。表里因此规定:单题提交次数原则上不超过三次,第三次还不过,就转入手动构造测试数据的环节。

第三个约束是遗忘曲线。昨天想明白的解法,今天大概率只剩下一个模糊印象。所以表里每天固定安排一段“复现”时间,不看题解、不看昨天代码,纯手写。这一段是最痛苦的,也是收益最高的。我把这段放在下午,因为上午攻坚之后脑子已经疲了,做新题的边际效率很低,但做复现刚好——它需要的是记忆提取而不是创造。

1.3 为什么别人排的表比自己排的管用

自己排的计划有个致命问题:它是可协商的。今天状态不好,那段就算了;这道题有意思,多写半小时。一天两天无所谓,一个月下来,计划表就变成了一张愿望清单。

带队的负责人排的表之所以管用,核心不在于他更懂算法,而在于他把判断权从执行者手里拿走了。几点切换、什么时候必须停、什么情况下允许看题解,这些全部前置规定好,执行的时候不需要再做决策。决策是要消耗意志力的,而意志力在连续解题的过程中消耗极快,等你把意志力花在“要不要再试十分钟”这种问题上,真正用来想算法的就不够了。

我在自己的表里加了两个硬规则,效果比任何技巧都明显。第一条是止损线:单题超过 45 分钟没有实质进展(连续两次提交分数不变,或者思路卡在同一个地方超过 15 分钟),立刻停止,去看题解的思路部分但不看代码,看完合上,自己重写。第二条是离屏时间:每 90 分钟强制离开屏幕 10 分钟,不刷手机,就是走动、喝水、看远处。这两条听着像养生,实际是为了保证后半天还有脑子可用。

2. 一天的节奏拆解:每段时间到底在干什么

2.1 早段:低强度输入,先把脑子热起来

早上的第一段不安排攻坚,安排的是“输入型任务”,大概 40 到 50 分钟。具体内容是把昨天标记过的题目重新读一遍题干和输入输出格式,不动手写,只在纸上写两样东西:这道题的数据范围是多少、我打算用什么复杂度。这一步看着简单,实际能筛掉一大半的低级错误。

我踩过的坑就在这里。刚上手那阵子,读题基本靠扫,看到“给定一个整数 n”就直接往下写,结果数据范围那一行写着 n 不超过 10 的 5 次方,元素值不超过 10 的 9 次方,两数相加直接爆 int。这类错误在 PTA 上会表现为“部分正确”,测试点里总有几个大数过不去,而你盯着代码看半天看不出问题,因为代码逻辑确实没错。

早段还要做一件事:把当天要用的代码模板过一遍。模板不是让你复制粘贴,是让你在动手前确认输入输出、数据类型、循环边界这些骨架部分没记错。我用的是一个固定的骨架,每次开工前默写一遍,两三分钟的事,能省掉后面半小时的调试。

2.2 上午段:高强度攻坚,一次只啃一个点

上午是全天效率最高的窗口,安排两段 75 分钟的攻坚,中间隔 15 分钟。这两段只做一件事:做那些需要真正想清楚的题,比如图论里的最短路变形、动态规划的状态设计、复杂模拟题的边界梳理。

这里有个反直觉的经验:攻坚段不要按“简单到难”排序,要按“能不能在 75 分钟内看到进展”排序。有些难题你知道自己想不出,硬啃只是浪费时间;有些中等题你一上手就知道有戏,那就先做它,把状态拉起来。我一般的排序是:一道有把握的 + 一道没把握的,交替着来。

攻坚段里有个具体操作很值得说:在纸上写状态定义,再写代码。动态规划尤其如此。状态转移方程写不出来,多半是状态定义错了,而不是你不够聪明。我在纸上会写三行:状态是什么、转移从哪来、边界是什么。这三行写不清楚,就不打开编辑器。这个习惯养成之后,我对“运行超时”和“答案错误”的恐惧少了一大半,因为大部分错误在纸上就被拦住了。

2.3 下午段:手写复现,把别人的解法变成自己的

下午安排 90 分钟的复现段,这是整张表里我最不想执行、但收益最大的一段。具体做法是:挑昨天或今早看过题解的一道题,合上所有参考资料,从空白文件开始重写一遍。写不出来的地方做标记,写完之后对照题解找差距。

这一步的价值在于“提取练习”。看题解的时候,大脑产生的是“识别感”——哦原来是这样,我懂了。但识别和提取完全是两回事。看懂别人的代码,和自己从零写出等价逻辑,中间隔着的正是真正的能力。我做过一个粗略统计,第一遍看题解觉得“完全理解”的题,合上重写能够一次通过的不到四成,剩下六成里有一半会卡在初始化、边界、下标偏移这些细节上。

复现段还有个变体,适合时间紧的情况:只写核心函数。完整代码太长的话,就只写那个关键的函数体,比如二分查找的边界判定、并查集的合并与路径压缩、拓扑排序的入度更新。这部分写对了,外围的输入输出都是体力活。

2.4 晚间段:复盘归档,把经验变成可检索的条目

晚上安排 60 分钟复盘,不写新代码。复盘要产出一个东西:一条可以被未来的自己检索到的记录。格式固定三行——这道题考的是什么、我卡在哪一步、下次遇到同类题的第一反应应该是什么。

举个具体的例子。有一类题要在无向图里判断连通分量个数,用并查集做,卡了我很久的点不是并查集本身,而是“怎么处理孤立点”。记录写下来就是:考并查集计数;卡在孤立点的初始化;下次先数点,初始化为 n,每成功合并减一。这条记录五十个字,下次再遇到同类题,三十秒就能想起来。

复盘还要做一件容易被跳过的事:清理提交记录里的错误分类。把当天所有非“答案正确”的提交拉出来,按错误类型归一下类。如果一天里“格式错误”出现了三次以上,说明你输出格式控制有大问题,第二天的早段就应该专门练这个,而不是继续刷新题。很多人复盘只看“这题会不会”,不看“我为什么错”,结果是同一个坑一周能踩五遍。

3. 题目分层与每周配比:别把时间花在无效刷题上

3.1 四层分类法,每一层的目标完全不同

把题目按“你从这道题里获得什么”分成四层,比按难度分有用得多。

层级特征单题目标耗时主要收益
基础层语法、输入输出、简单分支循环10 到 15 分钟手感与准确率
模板层排序、二分、并查集、最短路等固定套路20 到 30 分钟代码熟练度
综合层多个知识点串联,需要设计结构45 到 75 分钟拆解与建模能力
边界层专门考精度、大数、特殊输入30 到 60 分钟严谨性

这个分层的价值在于:它让你知道每一层该用什么心态去对待。基础层追求的是零失误,不是做出来就行,而是每一次提交都必须是“答案正确”,一次都不许有格式错误。模板层追求的是默写速度,目标是合上书本能把并查集写完且一次通过。综合层追求的是思路成立,分数不到位可以接受,思路错了才是真问题。边界层追求的是覆盖完整,把能想到的特殊情况列成清单逐条验。

我见过不少人卡在瓶颈,问题就出在层级错配:天天泡在综合层里,基础层的格式错误率却居高不下,导致综合题的分数永远卡在七八十上不去。这就像盖房子,地基没夯实就往上层堆材料。

3.2 每周配比与难度爬坡

一周的时间按 6 天排,留一天机动。工作日每天 4 到 5 小时有效训练,周末两天各 6 小时左右,其中一天做整卷模拟。

层级每周题量占比时间占比安排位置
基础层30%15%早段
模板层30%25%上午前段
综合层30%45%上午后段 + 下午
边界层10%15%下午后段

注意题量占比和时间占比是反过来的。基础层题多但简单,占三成题量只用一成半时间;综合层题量也是三成,却要吃掉将近一半时间。这个错配是故意的,因为你真正需要成长的地方就是综合层,基础层只是维持手感。

难度爬坡有个简单的判断标准:当前难度下,你的首提交通过率保持在 50% 到 70% 之间,就是合适的档位。低于 50%,说明题太难,做着做着就变成看题解,学习效率极低;高于 70%,说明题太简单,你只是在重复已经掌握的东西,没有新刺激。这个比例我调整过好几次,稳定在这个区间之后,进步速度明显比之前“随便挑题”快得多。

3.3 单题止损线和时间预算的具体算法

止损线不要拍脑袋定,用这个公式算:止损时间 = 该题目标耗时 × 1.5。目标耗时参考上一节的表格。综合层的题目标 60 分钟,止损线就是 90 分钟;模板层的题目标 25 分钟,止损线就是 37 分钟左右,超过就停。

为什么是 1.5 倍而不是 2 倍?因为超过 1.5 倍之后,你能获得的边际收益急剧下降。剩下的时间里你多半是在做两件事:反复改同一处代码,或者干脆发呆。这两件事都不产生学习。

止损之后做什么,也要提前规定好,否则你会陷入“停下了但不知道干嘛”的状态。我的做法是分三种情况:一是思路完全没有,去看题解的思路部分,看完合上自己写;二是思路有但实现不出来,去翻自己的模板库或者笔记,找同类题的实现;三是思路和实现都有但过不了测试点,转去手动构造边界数据,用打印中间变量的方式定位。这三种情况的处理方式完全不同,混着来就是浪费时间。

4. 一次完整训练日的实操记录

4.1 开工前的环境与模板准备

开工前五分钟做三件事:打开编辑器、确认语言和编译选项、默写一遍主函数骨架。PTA 上不同题目支持的编译器版本可能不一样,有的题允许较新的标准,有的只到较老的版本,提交前扫一眼题目页面的说明,能省掉一次莫名其妙的编译错误。

我的主函数骨架长这样,基本不用改:

#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; int a[MAXN]; // 大数组一律放全局,避免爆栈 int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; while (scanf("%d", &n) != EOF) { // 多组输入的标准写法 // 处理逻辑 } return 0; // 千万别漏,漏了就是"返回非零" }

这里有两个细节值得展开。第一,ios::sync_with_stdio(false)这一行是给 C++ 流加速用的,很多题的时间限制很紧,不加这一行用cin读十万级数据可能直接超时。第二,多组输入的写法,PTA 上不少题是“输入多组数据,直到文件结束”,如果你只读一组就退出,结果会是“答案错误”或者只过一部分测试点。

大数组放到全局这件事我要多说一句。局部变量分配在栈上,栈空间通常在几 MB 量级,你写个int arr[1000005]放局部,四兆字节,直接栈溢出,判题机报给你的是“段错误”,而不是“内存超限”。这个错我当年找了整整一个下午,最后才发现是把数组挪到全局就好了。

4.2 输入输出的细节,占低级错误的一大半

输入输出看着最简单,实际上是最容易掉分的地方。列几个高频问题。

关于混合使用cin和scanf:如果关了同步,两者混用会出问题,读入顺序错乱。要么全用 C 风格,要么全用 C++ 风格,别混。关于getline和cin >>:cin >>读完一个数之后,换行符会留在缓冲区里,紧接着调getline会读到一个空串。解决办法是在中间加一次cin.ignore(),或者干脆全部用getline读进来再自己解析。

关于输出格式:行末不能有多余空格,这是一条铁律。很多题的输出是“数字之间用一个空格分隔”,你最后多打一个空格,判题机比对字符串就直接判“格式错误”,但分数往往给一部分,让人以为是算法问题。我的做法是统一写成“第一个元素单独输出,后面每个元素输出前加空格”这种模式,从结构上杜绝末尾空格。

关于浮点数:不要用==比较两个浮点数,用fabs(a - b) < 1e-8。输出要求保留两位小数时,注意printf("%.2f")的行为,以及某些题目对“四舍五入”和“四舍六入五成双”的要求差异。涉及金额、比例的输出,先把样例手算一遍,对上了再提交。

4.3 提交、调试、定位的固定流程

我把提交后的动作做成了一套固定流程,避免瞎改。

第一次提交不过,先看结果码,不要看代码。结果码是“格式错误”,问题几乎必然在输出格式,去检查末尾空格、空行、大小写;结果码是“运行超时”,问题在复杂度或者死循环;结果码是“段错误”,问题在数组越界或者递归过深;结果码是“答案错误”且分数很低,问题在算法思路;分数很高只差一两个测试点,问题在边界处理。

定位“答案错误”的时候,最有效的办法是自己构造小数据。题目给的样例通常太简单,你得造那种“刚好能触发边界”的数据:n 等于 1、n 等于最大值、所有元素相同、元素有负数、图不连通、只有一个点没有边。这些小数据用打印中间变量的方式跑一遍,问题基本都能现形。

还有一个技巧:把printf调试法和分治排查法结合起来。先在代码里几个关键节点打标记,确认程序走到了哪一步;如果确认流程正确但结果不对,就把输入规模缩到最小,比如 n=2,手工推演一遍,看程序输出和自己的推演差在哪。这个办法笨,但是万能。

4.4 复盘表怎么写才有用

复盘表不是流水账,要能被将来的自己检索到。我的表有四列:

题目特征考查点卡点下次的第一反应
无向图连通块计数并查集孤立点没算进去先初始化为 n,每合并一次减一
区间查询最值线段树 / 稀疏表边界包含关系写反先确定左闭右闭还是左闭右开
字符串模式匹配KMP 的 next 数组next 数组下标从 0 还是 1 开始先看题解用的是哪种约定

第三列“卡点”是最值钱的一列。它记录的不是“我不会”,而是“我具体在哪一步错了”。一个月后翻回来,你会发现自己的卡点高度重复——可能八成的问题都集中在三种类型上:初始化、边界、下标偏移。发现这个规律之后,你可以在写代码时有意识地在这三处多检查一遍,效率提升非常直接。

5. 常见问题与排查技巧实录

5.1 判题结果速查表

下面这张表是我自己总结的,贴在显示器旁边,出问题先扫一眼。

结果码最可能的原因第一步该看哪里
答案正确通过记录思路,归档
格式错误行末空格、空行、大小写、缺换行输出语句
答案错误(低分)算法方向错重新读题,看数据范围
答案错误(高分)边界、特殊输入构造 n=1、极值、重复元素
运行超时复杂度过高、死循环、输入太慢算一遍量级,检查循环条件
段错误数组越界、递归过深、局部大数组数组声明位置、递归深度
浮点错误除零、对负数开方、取模零分母是否可能为零
返回非零主函数没写 return 0主函数末尾
编译错误语言版本、头文件、拼写提交时的语言选项

这张表的价值在于缩短“发现问题”的时间。真正耗时的从来不是改代码,而是不知道自己错在哪,反复提交试探。

5.2 运行超时的三条排查路径

运行超时是最容易让人误判的问题,因为代码看起来完全正确。按这三条路径走。

路径一:算复杂度。把数据范围拿出来,n 多大、边多少、查询多少次,乘一下操作总数。经验值是每秒能跑一亿次左右的简单操作,超过这个量级就得换算法。n 是十万、你要做双层循环,那就是一百亿次,铁定超时,这时候再怎么优化常数都没用,必须换思路。

路径二:查死循环。循环变量有没有在循环体内被正确更新,边界条件是<还是<=,while里的条件会不会永远为真。这类错误有个特征:小数据能过,大数据超时,因为小数据循环几次就结束了,大数据会一直转。看到“小数据通过、大数据超时”,先怀疑死循环。

路径三:查输入输出开销。这一条最隐蔽。算法复杂度是对的,但输入用了没加速的cin,或者输出用了endl频繁刷缓冲,几十万次下来就是几百毫秒。改用scanf/printf,或者关同步、把endl换成\n,往往能直接过。

5.3 段错误和浮点错误的现场处理

段错误的排查有个套路:先看数组,再看递归,最后看指针。数组方面,检查声明的大小够不够、下标有没有到 n 而不是 n-1、有没有负数下标;递归方面,检查终止条件是否可达、递归层数会不会达到几十万层;指针方面,检查有没有使用未初始化的指针。

有个特别隐蔽的情况:声明了int a[1000],题目数据是 1000 个元素,你从 0 填到 999 没问题,但某处循环写成了i <= n,访问到a[1000],越界一个元素。这种越界在有些环境下不会立刻崩溃,只是悄悄改掉下一个变量的值,表现出来是“答案莫名其妙不对”,查起来极其痛苦。所以数组一律开大一点,多开十个元素,几乎不花成本。

浮点错误相对少见,但一旦出现很有规律:找分母。除法的分母、取模的右操作数、开方的参数、对数的真数,逐个检查是否可能为零或负数。还有一种情况是类型转换,整数除法被截断导致后续计算全错,这种不会报错,只是结果不对,需要在小数据上手动验算。

5.4 节奏崩了怎么救

再怎么排表,也会遇到一整天状态稀烂的时候:题读不进去,代码写一行改三行,提交一次错一次。这时候最忌讳的是“硬撑”,撑到晚上只会得到一堆挫败感。

我的处理办法是把当天的任务降级,不是取消。攻坚段改成模板层练习,做几道熟练的题,找找手感;复现段改成读题解,不动手写;复盘段照常做。这样一天下来虽然没进步,但也没有断掉节奏,第二天恢复起来很快。

反过来,如果连续三四天都是这个状态,那问题不在状态,在安排。要么是难度档位调得太高,要么是训练时间排得太满没有恢复空间。这时候应该往回退一档,把首提交通过率拉回 50% 以上,稳定一周再往上走。我自己的经验是,爬坡爬得太急,退回来重爬花的时间比稳扎稳打还多。

最后说一个我个人一直在用的判断标准:如果一天训练结束,你能用三句话说清楚今天解决了什么问题、卡在了哪里、明天第一件事做什么,那这一天就是有效的,不管做了几道题。说不清楚,做二十道也是白做。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进