最近把这套19年区域赛的题单翻出来重新过了一遍,也就是常说的2019ICPC银川区域赛补题。说实话,补题这件事看着简单,实际做起来特别容易变成“对着题解抄一遍代码然后自我感动”。这篇博文就从实战角度聊聊我是怎么安排这类补题的:怎么给题目排优先级、每种题目从哪个方向入手、用什么流程把一道不会做的题真正变成自己的东西。适合刚打完比赛想认真复盘的选手,也适合自己拿题单训练却不知道怎么消化的初学者。
1. 补题之前先想清楚:这套题要怎么补才不白费
1.1 为什么补题比无脑刷新题更有性价比
区域赛的题目是命题组反复打磨过的,每道题背后基本都对应一个明确的考点和思维陷阱。比赛时没做出来,不完全是因为题目难,更多时候是某个知识点存在盲区,或者某种思考方式没建立起来。把这些缺口补上,比漫无目的地刷十道简单题要有用得多。
我见过不少人打完比赛就急着去刷下一套题,结果到下个赛季遇到类似的题照样卡住。原因很简单:赛场上暴露的问题没有被真正解决,只是被新题盖过去了。补题恰恰是把“当时没做出来”变成“现在能独立做出来”的最短路径,它直接命中你的薄弱点。
另一个价值在于,区域赛题目通常有比较合理的难度梯度。简单题帮你巩固基础,中档题训练建模能力,难题开拓视野。你单独去刷题很难找到这样结构完整的题单,直接补区域赛题等于用一套高质量训练材料做定向强化。
1.2 先把赛时情况还原成一张状态表
很多人补题的问题在于:不知道自己当初为什么没做出来。所以我建议补题前先做一次“复盘归档”,把每道题的状态填进一张表格里,再决定怎么补。
| 题号 | 是否读题 | 是否有思路 | 卡在哪个环节 | 赛后状态 | 优先级 |
|---|---|---|---|---|---|
| A | 是 | 有 | 实现细节写崩 | 已补 | 高 |
| B | 是 | 无 | 知识点不会 | 未补 | 中 |
| C | 否 | - | 时间不够没看 | 未补 | 低 |
| D | 是 | 有 | 思路方向错 | 未补 | 高 |
这张表的核心目的是把失败原因分类,常见的有四类:
一是知识点缺失,完全不知道要用什么算法,这类需要专门补理论基础;二是思路方向错,知道相关算法但没想到怎么建模,这类最值得优先补,因为思维上的突破对能力提升最大;三是代码实现差,想对了但写崩了,这类需要重写一遍代码,锻炼实现能力;四是时间分配问题,题目本身可能不难,但赛时没轮到,这类应该最快补掉,帮你建立“下次遇到能抢到时间”的信心。
1.3 怎么排优先级:先补“该会的”,再啃“硬骨头”
我采用的排序策略是这样的:
- 第一优先级:难度不高但赛时没做出来的题,这类通常是签到题或铜牌题,补起来快,性价比最高。
- 第二优先级:榜上很多人过、但你完全没思路的题,这类题代表基础知识的典型应用,不补会一直吃亏。
- 第三优先级:比较难的金牌题或防AK题,如果耗费时间太久,可以先看题解理解思路,不一定要完整实现。
时间上也要给自己定一个硬约束。我一般给每道题留30到60分钟的独立思考时间,超过这个时间还没进展,再去看题解也不迟。很多人的问题不是不够努力,而是一道题空想两小时,最后心态崩了直接弃疗,不如限定时间来得高效。
2. 按题型拆解补题重点:每类题到底在看什么
2.1 图论题:重点在建模,不是背板子
区域赛里的图论题,很少让你直接“跑一遍最短路”或“套一个网络流模板”。更多时候是给你一个看似和图无关的场景,需要你把它抽象成图模型。补图论题时,最该关注的不是板子怎么写,而是题目里的哪些信息最后变成了图中的点、边和边权。
常见建模套路我先列几个,都是我补题时反复遇到的:
- 限制条件连边:比如“A必须比B先完成”,可以转成拓扑序或差分约束。
- 0/1边权处理:边权只有0和1时考虑01BFS,比跑Dijkstra更快也更稳。
- 状态压缩成图:有些题需要把“当前状态”作为节点,转移作为边,比如分层图最短路。
- 依赖关系建图后用拓扑排序判环:判环不只是模板,关键是要想明白环对应原题中的什么冲突。
补题时,我习惯把每道图论题的建模过程单独写一段注释,记录“为什么这样连边”。下次遇到场景类似但表述不同的题,能快速联想起来。
2.2 数据结构题:先回答“每个节点维护什么”
线段树、树状数组、分块这类题,最容易出现的误区是:板子背得很熟,但不知道每个区间上要存什么信息、怎么合并。补题的时候一定要追问自己两个问题:build时每个节点维护哪些值;pushUp时这些值如何由左右孩子合并出来。
举个经典例子:维护区间最大连续子段和。单看节点值不是只存一个“最大值”就行,还要同时存区间和、从左边开始的最大前缀和、从右边结束的最大后缀和,以及区间最大子段和。合并时,当前节点的最大子段和等于max(左子最大子段和,右子最大子段和,左子最大后缀和+右子最大前缀和)。如果补题时没想清楚这一步,代码多半会在合并逻辑上漏条件。
另一个常见坑是数据范围和下标映射。比如离散化时原下标从0还是1开始,值域很大时要考虑偏移量。我见过不少补题的人,算法思路完全正确,最后挂在数组开小了或者忘记开long long上。这类错误虽然低级,但恰恰是最浪费时间的。
2.3 数论与组合数学题:小范围打表找规律,再回头证明
数论题是很多人的心理阴影。区域赛里常见的考点包括筛法、质因数分解、gcd/lcm相关性质、组合计数、容斥原理,进阶一点是莫比乌斯反演或杜教筛这类。我的经验是:遇到一下子没有思路的数论题,先别死磕公式推导,直接写个暴力程序把小规模答案打出来看规律。
比如某道题要求计算某个函数在n=1到20之间的取值,你列出表格后发现答案呈现某种递推关系,或者有明显的周期性,这时候再往数学方向验证,思路会清晰很多。很多公式不是凭空想出来的,而是先观察结果再倒推过程。
当然,数论题补题还有一点容易被忽略:要严格区分“猜结论”和“证结论”。赛时可以大胆猜,只要对拍没问题就敢交,但补题时一定要把证明补上,否则下次碰到变体题照样不会做。我通常会把关键公式的推导步骤抄在笔记里,哪怕只是把题解里的推导重新写一遍,也比光看不写记得牢。
2.4 字符串与计算几何题:先把边界条件写进注释
字符串题常用哈希、KMP、后缀自动机、回文自动机等,计算几何常用凸包、极角排序、点在多边形内判定等。这两类题有一个共同特点:细节特别多,稍不注意就错得莫名其妙。
以字符串哈希为例,最容易出错的是下标从0开始还是从1开始。我习惯在代码开头把哈希数组的下标约定写成注释,后面所有查询都按这个约定走,避免写着写着乱套。再比如哈希取模冲突,虽然双哈希能大幅降低冲突概率,但你不能只在赛后补题时依赖双哈希,更要理解单哈希可能被卡的原因。
计算几何更不用说,几乎所有题都要跟精度较劲。判断点是否在多边形内、计算两条线段的交点,eps设大了会误判,设小了在浮点误差下又不可靠。补这类题时,我会专门整理一篇“精度处理清单”,记录当前这道题里哪些比较必须用eps,哪些可以直接用整数运算避免浮点误差。几何题里能用整数算就别用double,能用叉积判方向就别算夹角,这些经验都是一次次WA换来的。
3. 一套我实测比较好用的补题流程
3.1 第一步:不看题解,先写“思路草稿”
拿到一道待补的题,哪怕赛时完全没做出来,我也不会立刻去看题解。先强制自己独立思考30到60分钟,把能想到的所有方向都写在纸上:可能的算法、预期复杂度、大概怎么建状态、边界条件怎么处理。就算最后没写出来,也会在卡住的位置留下标记,比如“我本来想DP,但不知道怎么表示选择顺序”。
这一步的价值在于:等你再看题解时,你一眼就能看出自己的思维差在哪里,而不是只得到一个“原来如此”的模糊印象。我做过的对比实验是,先想过的题比直接看题解的题,记忆留存率高很多,之后重做时能更快回忆起关键点。
3.2 第二步:卡着时限亲手实现一遍
看懂了题解不等于会做。我的标准是:合上题解,自己把代码完整敲出来,并提交评测。这一步完全模拟赛场环境,要求一次只能看题面,不能看题解代码。如果实现过程中卡壳了,回去看自己写的思路草稿,不到万不得已不再看题解。
很多选手的问题在于“看得懂,写不出”。看懂一份题解只需要理解它的逻辑,但自己写要处理各种边界条件、变量初始化和数据结构细节。亲手实现一遍的意义就是把这些细节暴露出来,逼你在补题阶段就解决掉,而不是留到下一次赛场。
耗时的题也别着急。见过有的题解是O(nlogn),但你实现出来常数很大,本地跑大数据超时。这时候正好练习卡常数:数组开静态、快读快写减少IO、能用迭代就别递归、调整循环顺序。这些优化技巧在赛场上同样重要。
3.3 第三步:用小数据对拍和极端测试
代码写完AC了,大多数人就停在这儿了。但我的建议是再补一步对拍,特别是那种题意容易理解偏差、边界条件多的题。所谓对拍,就是写一个暴力程序(保证正确但可能很慢),再用一个随机数据生成器,把两个程序的输出反复比对。
while true; do python3 gen.py > in.txt ./brute < in.txt > out1.txt ./solve < in.txt > out2.txt if ! diff -q out1.txt out2.txt > /dev/null; then echo "WA found" cat in.txt break fi done对拍脚本的逻辑很简单,核心价值在于它能帮你发现“感觉对了但某个小细节错了”的问题。比如排序边界写反、map默认值未判断、long long溢出等,这些就算样例全过也发现不了。补题时做一次对拍,比无谓地多提交七八次要高效得多。
极端测试则更简单:构造n=1的情况、n取最大上限、所有值相等、完全逆序、答案本身就是最大或最小值等情况,直接跑一遍,看代码能不能稳定输出。我经常在“n=1”这种极端情况上翻车,原因往往是初始化了一个空集合但之后没有特判。
3.4 第四步:对照题解,补一份“题解笔记”
AC之后再看题解,不是看你哪里错了,而是看官方的思路和你的思维差在哪里。我习惯用一段话总结这道题的本质:这题其实考的是XX算法,关键性质是XX,转化方法是XX。然后把“我当时卡住的地方”和“题解如何绕过这个卡点”对应起来写清楚。
另外,把题解里更简洁的实现方式也记下来,比如有人用set取代了手写平衡树,或者用滚动数组压缩了维度。这些实现层面的取舍,看一次不一定能掌握,写进笔记后过两周再翻,效果会更好。
4. 补题时最常见的坑和排查方法
4.1 题意读错了怎么发现
补题时最容易出现的一种情况是:感觉自己理解了题意,代码也写得顺,结果提交后WA。这时候先别怀疑算法,回头把题面再逐句读一遍,重点看样例输入输出是怎么从题面描述推出来的。如果样例能手动推通,说明题意理解基本正确;如果手算结果和样例对不上,那肯定哪里理解偏了。
一个非常有效的偏方是故意给自己出反例。比如题目说“可以选择任意顺序”,但你的理解变成“必须按下标顺序”,试着构造一个需要非下标顺序的输入,跑一下自己的程序,马上就露馅。补题阶段反正没人催你,多花几分钟验证题意,等于省下后面几小时的调试时间。
4.2 本地能过,交上去却WA
这个问题的技术原因就那几类:未初始化变量、数组越界、多组测试数据没清空、long long溢出、浮点误差。排查的时候从最老的毛病开始查。
如果本地随机数据过了,但线上WA,我会先用编译器自带的一些检查选项重新编译一遍,像未定义行为和数组越界这类问题通常能被直接揪出来。然后在提交代码里加几个“防御性断言”,比如关键数组下标先判断边界再访问,多组数据时把要用到的容器在每组开头全部clear。如果是浮点问题,检查有没有把double直接和0比较,或者计算叉积前有没有统一精度。
线上评测环境跟本地环境也可能有差异,比如int范围、long long长度在常见平台是一致的,但编译器优化可能暴露未定义行为。所有循环变量、临时变量都要显式初始化,不要依赖默认值。
4.3 题解看不懂怎么办
题解写得简略是真正常见的问题,特别是那种“显然可得”“不难发现”的表述,对初学者非常不友好。我自己的办法是把题解拆成一个小句子一个小句子看,每句话都尝试翻译成数学表达式或代码里的一个变量定义。比如题解说“考虑以i为结尾的子串”,那就在草稿上定义dp[i],再写下转移等式。
遇到彻底看不懂的知识点,比如莫比乌斯反演或后缀自动机,先不要在这道题上死磕。标记一个“需要补前置知识:xxx”,把它放到学习清单里,先去找同类但更简单的题练手,回头再看这题,往往一下就通了。另一个办法是找队友或同好讨论,几个人各自理解题解里的一部分,合起来基本能拼出完整思路。
4.4 补题没有动力、速度太慢怎么办
补题确实是个枯燥的过程,特别是当题目很难、看到题解都费劲的时候。我的做法是严格限制单道题的投入时间,超过预期目标就不强求,可以把“看懂题解”作为低配目标,“独立实现”作为高配目标,根据当天状态选择。这不算妥协,每天的认知资源有限,硬扛两个小时不如分成两天各一小时。
还有一点小技巧:把补题跟刷简单题穿插进行。连续补三小时难题会让人很挫败,中间穿插几道快速能AC的简单题,保持手感之余心情也会好很多。补题计划不要太满,每天一到两题已经很不错,重点是每道题都补出质量,而不是数量上冲刺。
5. 把补题成果沉淀下来:建立自己的复盘体系
5.1 一题一页的复盘卡片怎么写
我自己的习惯是给每道补完的题建一张复盘卡片,内容包括:题目来源和题号、算法标签、难度自评、卡住的原因、一句话题意、核心思路三步走、代码实现中踩过的坑、下次重做的时间点。形式不固定,可以用表格,也可以写成多行文字,关键是方便日后检索。
举个例子,一张卡片可能是这样的:
- 标签:线段树、最大子段和、合并信息
- 题意:维护区间最大连续子段和,支持单点修改
- 思路:每个节点存sum/lmax/rmax/best,pushUp合并时注意跨界情况
- 踩坑:查询跨左右区间时也要做和pushUp相同的合并,不能直接返回左右结果
- 下次复习:两周后重做一遍,重点测试所有更新都发生在同一侧的情况
这种卡片的方便之处在于,复习时不需要重新读一遍完整题面,只要扫一眼卡片就能回忆起整个题目的骨架。我的经验是,当你能用两三句话把一道题讲清楚时,才算真正掌握了。
5.2 如何沉淀成可复用的模板和笔记
补题过程中会频繁见到一些经典算法的实现。与其每次现查现抄,不如整理成自己的模板库。我之前整理过一个常用的算法模板库,每个模板都带上适用条件和常见坑,比如“01BFS适用边权为0/1的图”“线段树开4倍空间”“网络流注意反向边容量更新”。
不过这里我不太建议直接复制别人的模板,最好自己手敲一遍。手敲的过程会迫使你理解每行代码的作用,也方便你按自己的习惯调整变量命名。等到赛场紧张的时候,只有熟悉的代码才敲得快而不出错。补题是磨练这套模板库的最佳机会,因为你有足够的时间反复修订。
5.3 隔周复习,否则补题等于白补
人的记忆曲线非常残酷,补完一周不看基本忘掉大半。我的习惯是补完当周先做一次轻量复习,只看复盘卡片,不看代码;过两周再挑其中最典型的一两题重新提交一遍,最好能做到不看自己当时的代码直接重写。这个过程很像刻意练习,每次重写都会发现之前遗漏的细节,比如对某个边界条件的处理,或者某段代码的常数优化。
过一个月再翻一次笔记,把仍然觉得陌生的题目标记出来,安排第二轮补题。这套“补题-复盘-重写-再复盘”的周期,能大幅提高补题的长期收益。如果只是把题解看一遍然后永远不再碰,那基本等于没补。
我个人在实际操作中的体会是,补题这件事,真正的门槛不在于题有多难,而在于愿不愿意把一道题多花几小时去“消化”。2019ICPC银川区域赛那套题,到现在还是我复盘本上的常客,每隔一段时间翻出来重新看,总能发现新的理解角度。希望这篇补题流程能帮你把那几道“当时没做出来”的题,变成真正属于自己的能力。