很多人对“补题”的理解就是赛后把比赛里没做出来的题 AC 掉,但真正高效的补题不是“对着题解把代码贴上去”,而是把整场比赛用接近正式赛的方式重新跑一遍,再去处理没搞定的部分。Codeforces Round 1083 (Div. 2) 就是这么被我翻出来的:它不是一个热门的轮次,难度分布却很典型,拿来练虚拟参赛再合适不过。虚拟参赛会让你自带时间压力和提交记录,比赛结束后的补题又正好落在同一个题目集合上,整个过程非常闭环。这篇文章就记录我用这轮比赛做的一次完整虚拟参赛和补题复盘,也把里面可以复用到其他题目的方法一起整理出来。
1. 为什么我会从题库里翻出 1083 来练虚拟参赛
1.1 这轮比赛适合作为训练标的的三个理由
Codeforces 上每天都有大量题目,新题不断出现,我身边不少人的补题策略是“跟着群里的讨论走,今天被人推荐哪题就做哪题”。这种方式的随机性太大,今天做一道区间 DP,明天做一道字符串 Hash,知识点的覆盖面看起来很广,但真正对比赛能力的提升很有限。
我选 Codeforces Round 1083 (Div. 2) 作为训练对象,首先是因为它的题目梯度比较完整。Div.2 的 A、B 通常是在考察代码基本功和题意理解,C 开始进入到常见的算法模型,D 和 E 则留给有经验的人。拿这样一轮完整的比赛去虚拟参赛,相当于用两小时强制自己经历一次“从简单到困难”的完整心理过程,这比零散刷十道题更能暴露问题。
第二个原因是它属于“有一定年头但没过时”的题目。过老的题可能充斥着大量边界条件、奇怪的交互格式,而过新的题又容易被讨论区剧透,或者带有很强的近期趋势。像 1083 这种处于中间状态的轮次,题目本身依赖的算法仍然是当前比赛的主力内容,没有太多偏题怪题,很适合用来磨稳定的解题节奏。
第三个原因是这轮比赛我本身没有参加过正式场。虚拟参赛有一个前提,就是不能提前看过题解或者提交过代码,否则整个模拟就失去意义了。选一场自己完全没碰过的比赛,能保证所有提交记录都是干净的,复盘的时候也能从零还原自己的思考路径。
1.2 虚拟参赛的规则,其实比想象中更适合补题
Codeforces 的虚拟参赛机制是在比赛结束以后,让你从比赛开始的时间点重新计时,用两小时或者题目规定的时长去完成整场比赛。所有提交都会走真实的评测环境,但分数不计入你的 Rating。
我第一次用这个功能的时候,以为它就是“限制时间的普通做题”。后来发现差别很大:普通做题时我可以随时停下来查资料,纠结一道题半小时也没关系,反正没有人在计时;虚拟参赛则会把正式比赛的约束条件完整地压过来,你要自己决定读题顺序、分配每道题的时间、控制心态,还要面对一次又一次 Wrong Answer 带来的挫败感。
这些约束恰好是补题最需要的。很多人赛后补题效率低,就是因为没有“比赛状态”。一旦开启虚拟参赛,你就会本能地要求自己在 A、B 这类简单题上少花时间,把多余的思考留给后面的硬题。这种压力不是坏事,它才是补题能真正转化为能力的核心。
需要特别注意的是,Codeforces 虚拟参赛要求在此之前没有对该场比赛提交过任何有效代码。如果你在比赛刚结束时就迫不及待地去“试水”提交了一题,那么之后再想开启虚拟参赛就会受到限制,会出现提示让你直接进入题目页面,但成绩不会被记录成一场完整的虚拟参赛。所以我现在的习惯是:只要决定要补某一场,就一定先忍住,不要在正式比赛结束后立刻去翻题、提交,等到有时间完整模拟的时候再一次性跑完。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开赛前 20 分钟,我到底在准备什么
2.1 一次完整的虚拟参赛要有明确的“题目边界”
不确定是否有人和我一样,刚开始虚拟参赛时非常随意:手机放在旁边,电脑上还挂着好几个聊天窗口,时间到了就直接点“Start Virtual Participation”。结果就是做题过程中总被打断,两小时的比赛硬生生拖成三个半小时,D 题明明有了思路却被一条消息打断,回来就忘干净了。
后来我给自己定了一个规矩:虚拟参赛的前 20 分钟不能浪费在题目上,而是用来设置“边界”。这个边界包括时间边界、环境边界和知识边界。
时间边界是给自己一个完整的、不会被占用的大块时间。我通常选在周末上午或者晚上,先把可能到来的电话、消息、快递等琐事处理完,然后打开手机计时器,设定与比赛时长相同的倒计时,并把手机倒扣在桌面上。
环境边界是指把工作区切换成比赛模式。我只保留一个浏览器窗口用于查看题目和提交代码,一个代码编辑器,一个终端用来本地自测。聊天软件、社交平台、视频网站的标签页全部关掉。这个动作看起来很小,但对专注力的影响非常直接。
知识边界指的是我允许自己使用哪些辅助工具。Codeforces 正式比赛允许查阅语言文档,但禁止查看别人已经提交的代码和题解。虚拟参赛虽然不是官方正式比赛,但我会按照同样的规则要求自己:不能开题解,不能看通过代码,也不能搜索这道题的解法讨论。只有在这种约束下暴露出来的不足,才值得后面花时间去补。
2.2 本地环境与时间闹钟的固定动作
每次虚拟参赛前,我还会在本地建一个专门的目录,目录名直接叫题目所在的比赛编号,比如这场就命名成 1083。然后在目录里准备好几个空白源代码文件,分别是 a.cpp、b.cpp、c.cpp、d.cpp 和 e.cpp。这个动作的用意是提前把“建文件、起名、保存”这种琐碎操作全部排除在比赛时间之外。
本地环境的准备还包括编译命令和测试方式。我用 C++ 写比赛代码,给编辑器绑定好一键编译运行的快捷键,确保自己不会在编译命令上浪费任何一秒钟。对于需要暴力验证的小数据,我也准备了对应的 Python 脚本模板,方便在遇到构造题时快速生成测试数据。
准备时间闹钟也是固定动作之一。Codeforces 虚拟参赛在你点击开始时就会自动在页面上倒计时,但那个倒计时藏在页面角落,很容易盯着题目就忘了看。我仍然会另外打开一个手机或者桌面计时器,设定为比赛总时长。这样即便某个页面刷出新状态,我也能知道还剩多少时间。
这 20 分钟做完之后,我一般不急着马上点开始,而是先站起来接杯水、上个厕所,让自己在比赛开始时处于一个身体状态稳定、注意力集中的节点。虚拟参赛毕竟拼的是两个小时的持续输出,前面那几分钟的准备,往往决定了这场比赛能投入多少。
3. 两小时的比赛过程,最值得记录的三个时间节点
3.1 前半段:快速把 A/B 用掉的安全感
虚拟参赛开始后,读题顺序我几乎没有犹豫:按照 A、B、C、D 的顺序逐题往下走。很多人喜欢先扫一遍所有题目,再决定先做哪道,这种做法在部分比赛里确实有效,但对 Div.2 的前几题来说,顺序做题能减少来回切换题目的心智负担。
A 题我给自己定的预算是 15 分钟以内。这轮比赛 A 题的代码量和思维量都不大,真正要注意的是输入的范围和输出格式。我在写代码之前会先把题目里的限制条件用笔圈一遍,特别是变量的上下界,因为这些数字往往决定了答案该用 int 还是 long long,也决定了一些中间过程会不会溢出。
B 题是这场比赛里最容易让人产生“假安全感”的题目。AC 前几题的节奏确实能带来信心,但这种信心也很危险,它容易让人在 C 题上盲目追求速度,结果连题意都读偏了。所以我的习惯是:A/B 做得再顺,提交前也会在本地多跑一两组自己构造的边界数据,而不是只依赖题目给的样例。
关于提交前自测,我有一套固定做法:先把样例跑通,然后根据题目范围取最小值、最大值分别构造两组输入。如果这两组输入的结果符合直觉,我再提交。这个过程通常只需要两三分钟,但能有效减少因为“没读清输出要求”导致的 WA,避免因为低级错误白白浪费提交罚时。
3.2 中段 C 题卡住:我没有马上开题解
到了 C 题,我的速度明显放慢。这题需要的不再是单纯的模拟,而是要找到一种构造方式或者贪心策略。我在草稿纸上尝试从几个不同的小数据开始推规律,比如当某个参数取 1、2、3 时,答案分别应该是什么。把这些结果列出来之后,原本混乱的思路会变得清晰一些,因为你能从具体输出里看到模式,而不是一直停留在抽象的“该怎么构造”上。
这种用暴力跑小数据来找规律的办法,在 Div.2 的 C 题里非常实用。一旦发现某种贪心策略在小数据上得到的结果和预期一致,我会再尝试构造一个反例去攻击它。只有反例攻击失败,我才会把这个策略写成正式代码。
如果过了 30 分钟还没有形成完整的解法,我不会继续死磕下去,而是做一次快速取舍:先跳过 C,去读一下 D 题,看 D 题是否是自己熟悉的模型。这个跳题决策很重要。虚拟参赛不是考试排名,它最大的价值是帮助你了解自己的临场时间分配。与其在 C 题上磨到比赛结束,不如先看看后面的题目有没有更容易拿下的分数。
实际上我在这一轮里也做了类似的调整。C 题卡顿之后,我先浏览了一遍 D 题的题意,确认它的数据规模和模型类型后,再决定把主要精力放在哪里。最终 C 题在比赛时间内完成,D 题则带着一个不够严谨的思路进入了补题阶段。
3.3 后段 D 题:输出答案前的约束检查
D 题通常是一个分水岭。这道题能不能做出来,往往不是考验你会不会某个高级数据结构,而是考验你能不能从题目描述里抽出真正的数学模型。我在这轮比赛剩余的大约 40 分钟里尝试了 D 题,第一版思路完全是顺着样例的形态去猜的,结果本地测试时就连构造的样例都过不了。
发现思路有问题之后,我并没有立刻换方法,而是把题目重新读了一遍,找出所有可能产生歧义的句子。D 题常见的坑在于“子序列”和“子串”的表述区别、取模范围的细节,以及输出答案是否需要排序。这些问题如果读题时不留意,写出来的代码方向可能从头就是错的。
这里还要提一个很实际的教训:输出格式和边界条件,一定是在写代码前确认,而不是等写完代码再对照。我过去常犯的一个错误是,题目都没彻底看完就开始写框架,写到中间才发现漏了一种情况,然后把已经写好的代码推倒重来。这种情况在虚拟参赛里尤其致命,因为它不仅浪费实现时间,还破坏了心态。
这一轮 D 题在比赛结束时没有通过。最终提交状态停在了“比赛内未完成”,这本身也算是一种有价值的记录,因为它清楚地告诉我:现在的能力边界在 C 到 D 之间。
4. 比赛结束后的补题顺序,决定一次训练是否有效
4.1 用提交记录而不是记忆还原卡点
虚拟参赛结束后,很多人的第一个动作是马上看题解,我的建议是先别急。两小时的高度集中会让大脑产生一种错觉,好像每一道题的想法都还记得很清楚,但只要隔一个晚上,这些记忆就会变得模糊。真正有价值的复盘,应该趁热打铁,但不能用“看题解”来趁热。
我会先打开比赛页面里的“My Submissions”,逐条看自己的提交记录。每一份 Wrong Answer 背后都对应着一个当时的想法,我要做的是把这些想法重新还原出来:第一次 WA 是因为边界条件没写好,还是因为算法本身错了?第二次提交修改了什么?为什么修改之后仍然失败?
这个还原过程比想象中的更有收获。有时候你会发现自己浪费了三次提交,其实都是因为同一个误解;有时候你会发现自己刚开始的想法才是对的,只是没有坚持验证下去,中途被另一个看似更巧妙的思路带偏了。补题如果只看题解和最终通过的代码,这些隐藏在提交记录里的信息就全部丢失了。
还原卡点之后,我会把每一道题的关键状态记录在一张便签上。比如哪道题读了 10 分钟才理解题意,哪道题是样例输出诱导了错误思路,哪道题是代码实现时某个变量名写错导致长时间排查。这些记录不需要写得多漂亮,只需要在第二天复习时能一眼看懂。
4.2 从看题解到关掉题解写代码之间,隔着一层翻译
经过上面的还原,真正不懂的题目可能只剩一两道。这时候再去看题解,效率会高很多。因为你不是在漫无目的地寻找答案,而是带着具体的困惑去看题解,所以能很快定位到自己没想到的那个关键点。
但看题解有一个陷阱:看懂和能写出来之间差距很大。题解往往只给一个抽象的结论,比如“用并查集维护连通块,并按权值排序”,但具体到题目里的下标怎么处理、相等元素怎么判断,题解通常不会展开。如果只是“看懂了”就开始下一题,下次遇到类似题目时,你仍然会卡在实现层面。
我给自己定的规则是:看完题解后必须合上题解页面,重新打开一个空白代码文件,从头写一遍完整实现。写成什么样才算合格呢?第一,能在本地通过我随手构造的边界数据;第二,能一次性通过评测;第三,如果需要反复修改才能过,那就说明题解里的关键思想还没有真正变成自己的。
对于 D 题,我是先读了题解里关于算法的方向提示,然后合上页面自己重新推了一遍。这个过程里我又犯了两个错,一是漏掉了一种特殊情况,二是把某个循环的边界写错。这两个错恰好说明我读题解时没有注意到这些细节,直到自己动手时才暴露出来。补题阶段暴露这些问题,总比正式比赛里再次踩坑要好得多。
4.3 二刷检测:几天后再跑一次才是真掌握
仅靠补题当天的 AC,还不能证明这道题已经掌握。程序竞赛里常见的情况是:当时看着题解写出来了,自我感觉良好,过一周再见到同类型的题照样没有思路。正因为如此,我会把这场里所有补出来的题都记录到一个复习列表里,并安排一次二刷。
二刷的时间通常选在补题后的三到五天。二刷的规则比虚拟参赛更简单:不看题解,不看自己补题时写的代码,只根据题目名称重新入手,目标是重新 AC。如果二刷仍然能顺利通过,说明这道题的核心思路已经形成了长期记忆;如果完全忘了,那就说明第一次补题时只是在“抄写”,而不是在“理解”。
这种二刷机制让我避免了一种很常见的虚假努力感:刷了很多题、补了很多题,但真到比赛时能随时调用的算法仍然很有限。二刷不追求数量,一周能完成 3 到 5 道题的复现就已经很好,关键是每一道都经得起遗忘曲线的考验。
5. 虚拟参赛加补题这个组合,最值得警惕的几个坑
5.1 把虚拟参赛做成“开卷模拟”
虚拟参赛最大的价值在于模拟正式比赛的信息约束,但很多人会无意中破坏这种约束。我这里说的“开卷”,不是指开题解,而是指带着各种平时积累的资料去做题。
比如有的选手会把模板库打开,看到题目就想往模板库里找现成代码。遇到扫描线就翻线段树模板,遇到字符串就翻后缀数组模板。这种做法在虚拟参赛里会让思维产生依赖性——反正模板都在手上,就不需要深入理解算法的原理,也不需要掌握代码的细节。一旦到了没有模板可用的正式比赛环境,或者代码库里缺少某个封装,问题就会集中爆发。
我的做法是:虚拟参赛过程中允许使用自己平时在用的模板,但每一份模板代码都必须在赛前背得很熟,至少能知道它适用的条件和复杂度。平时做题时如果发现自己频繁依赖某个不懂原理的模板,那就应该专门花时间把这个模板彻底吃透,而不是继续在比赛时“开卷”。
5.2 只追求 AC,不保留过程
另一个我反复踩过、也反复提醒自己的坑是:只看结果,不看过程。有些题虽然提交 AC 了,但实现过程非常艰难,中间改了七八次,有些甚至只是因为侥幸的边界条件才通过。如果只看最后的状态是 Accepted,就会错误地高估自己对这道题的掌握程度。
补题阶段我会在每道题旁边记录提交次数,并把第一次 AC 的时间点写下来。如果某道题提交了三次以上才通过,我就会在心里把它当作“WA 过一次但最终解决”的题目来处理,要求自己在二刷时做到一次通过。这样做的好处是,真实的做题水平不会被表面的绿色 Accepted 掩盖。
5.3 补题永远优先于刷新题
最后一个经验是关于训练节奏的。很多人喜欢把时间花在新题上,因为每 AC 一道新题都会带来新鲜感和成就感。但从长期能力提升来看,补题带来的收益往往高于刷新题。新题刷得再多,如果都停留在舒适区,水平很难突破;而一道让你卡了两个小时的题,哪怕最终没有独立做出来,它能带给你的思维刺激也远超过十道轻松 AC 的题。
所以我在安排每周训练时会把补题放在优先位置:先完成上一场虚拟参赛的遗留问题,再去刷新的题。Codeforces Round 1083 (Div. 2) 这场虚拟参赛,表面上的结果是“三道 AC 加一道补题通过”,但真正让我得到提升的,是赛后的那几轮复盘和二刷确认。比赛本身只是把问题暴露出来,补题才是把一个模糊的“不会”变成一个清晰的“会”的过程。
