一道简单题,两个世界
cpp
// 选手A的刷题代码
int main() {
int problems_solved = 0;
while (problems_solved < 3000) {
solve_next_problem(); // 刷过去,从不回头看
problems_solved++;
}
bool in_provincial_team = false; // 三年过去,还是 false
return 0;
}
// 选手B的刷题代码
int main() {
int ability = 0;
while (ability < PROVINCIAL_TEAM_LEVEL) {
Problem p = select_problem();
p.solve();
p.deep_review(); // 每一题都榨干价值
ability += p.gained_insight();
}
return 0;
}
如果你能看懂上面这段代码,你一定也在困惑:为什么有人刷题过千却止步不前,有人看似轻松却能直通省队?在信息学竞赛这条路上,最残酷的真相莫过于:刷题数量与能力提升之间,并不是线性关系,甚至可能毫无关系。
当你在洛谷的提交记录突破3000大关,当你的代码量足以写一本小说,当近三年的真题集已被翻得卷边——省队名单上依然没有你的名字。这不是因为你不够努力,而是因为你忽略了竞赛进阶中最核心的引擎:复盘。
一、刷题3000道的真相:当努力变成泡沫
在浙江、江苏、广东这些信奥强省,想要挤进省队,稳定攻克紫题(省选/NOI难度)是起码的门槛。紫题所考察的,已不再是基础知识的简单应用,而是网络流、后缀自动机、树套树等高级算法,以及背后深刻的数学模型和巧妙的转化思想。
那么问题来了:为什么刷了3000道题,却连紫题的门都摸不到?让我们用C++的思维来审视那些“无效刷题”的代码模式。
陷阱1:无限循环在舒适区
cpp
while (able_to_solve_easy_problems) {
solve_problem(difficulty = "熟悉题型");
confidence++;
}
// 能力曲线早已收敛,却还在空转
很多选手沉迷于刷自己擅长的题型——擅长模拟题,就一口气刷几百道;熟悉线段树,就只找线段树的题。看着AC的绿色标记产生虚幻的成就感,殊不知这就像在代码里写了一个死循环:CPU满载,却没有任何实质输出。
真正的高手会告诉你:做题的难度,应该选那些需要花费四十分钟到一小时才能做出来的题。太简单的是无效重复,太难的是无效挣扎。
陷阱2:只声明,不定义——眼高手低的“函数声明”
cpp
// 看到题解后
void solve_hard_problem(); // 只声明,不定义
int main() {
solve_hard_problem(); // 链接错误:undefined reference
return 0;
}
“这道题我看了一眼题解,思路懂了,就不写了。”——这是最危险的undefined reference。思维到代码之间隔着千山万水:边界条件、空间优化、常数因子、数据类型溢出……只有真正动手定义(实现)那个函数,编译器才会给你生成可执行文件。
四川大学的李祉橙在CSP认证中就因为一个int没改成long long,查错花了一个小时。他事后反思:“真没想到大学的第一场测试就碰上‘爆int’这个老朋友”。只看不写,等于只声明了能力,却永远无法链接出真正的实力。
陷阱3:魔法数字与硬编码——思维固化的模板依赖
cpp
if (problem_type == "区间查询") {
use_segment_tree(); // 不管数据范围,不管是否在线
}
看到区间查询就套线段树,看到最短路就写SPFA——这就是代码里的“魔法数字”,写死了思维。真正的优化是根据题目性质选择最合适的数据结构,甚至有时候暴力算法才是正解。
二、什么是真正的“复盘”?——像调试代码一样解剖自己
在C++编程中,当程序输出错误,我们会做什么?调试(debug)。单步执行、查看变量、回溯调用栈,直到找到那个导致崩溃的指针。复盘,就是对你解题过程的调试。
2.1 复盘的调试三步骤
第一步:复现错误(Reproduce the bug)
模拟赛后,不要只看分数,立刻重现当时的思维过程。在脑海里或草稿纸上单步执行:
- 读题时,哪些关键词被我忽略了?
- 推导样例时,哪一步算错了?
- 写代码时,是哪个变量让我陷入了死循环?
第二步:定位错误根源(Locate the root cause)
像用GDB打断点一样,在时间轴上找到“卡顿点”:
cpp
// BUG#01: 在第二题上耗时 1h 23min,实际应 30min 后放弃 // BUG#02: 对拍时发现大数据会 RE,数组开小了(N=1000 但实际输入 2000) // BUG#03: 推导 DP 方程时漏掉了 k=0 的情况
第三步:修复并验证(Fix & Verify)
- 下次遇到类似难度题,设定最大思考时间30分钟
- 以后数组大小一律
N+5,防止越界 - 写DP前先枚举所有边界情况,用小数据验证
2.2 复盘的“代码审查”视角
顶尖OIer的复盘不止于调试,还会进行代码审查(Code Review)。对比自己的代码和大佬的标程:
- 变量命名:我的
i、j、k满天飞,标程用left、right、mid清晰明了 - 模块化:我的
main函数写了200行,标程拆成了init()、solve()、output() - 复杂度:我的代码虽然AC,但跑在超时的边缘;标程用了更优的算法,常数极小
这种审查会让你意识到:AC只是及格,优秀才是省队的入场券。
三、高效复盘的三个层次:从“编译警告”到“架构重构”
就像C++代码的优化有不同级别(O0、O1、O2、O3),复盘也分三个递进的层次。
第一层:技术性复盘——消灭编译警告与运行时错误
这是最基础的-Wall -Wextra级别。目标是把所有“潜在风险”扼杀在摇篮里:
- 未初始化变量:比如
int ans;直接累加,导致随机值 - 数组越界:
for (int i=1; i<=n; i++) a[i]却开了a[100],n=101时就崩了 - 整数溢出:
1e9 * 1e9直接赋值给int - 文件操作:
freopen的路径写错,导致爆0 - 浮点精度:
if (sqrt(disc) == floor(sqrt(disc)))可能因精度失败
技术性复盘就是把这些warning变成error,强迫自己写出健壮的代码。在刷题中,对应的是:
第二层:策略性复盘——优化时间分配与得分策略
这一层对应编译器的-O2优化,目标是让程序跑得更快、更省资源。更要紧的是,OI赛制有个特点:可以获得部分分。每道题有多个测试点,根据通过的测试点数量获得相应分数。
在CSP/NOIP这种OI赛制的比赛中,策略往往比能力更重要:
- 难度误判:那道你花了1小时没做出来的题,实际难度级别是什么?如果重来一次,你应该在第几分钟果断跳过?
- 暴力与正解的权衡:时间不够时,你是否写了暴力程序来拿部分分?很多时候,省队与一等奖的差距,往往就在于最后30分钟里,你是在死磕正解,还是在有条不紊地书写暴力程序骗分
- 读题偏差:是否因为没模拟样例就写代码,导致全盘皆输?李祉橙在CSP认证中就因为读错题目,白写了一个树状数组优化的DP
CSP满分选手的建议是:“无法拿满分的题,可以考虑先拿80分,不必死磕于最后的20分”。
第三层:思维性复盘——重构算法直觉与知识体系
这是-O3甚至手动汇编优化级别,触及算法竞赛的本质——思维建模。
一题多解:这道题除了用线段树,能不能用树状数组?除了用后缀数组,能不能用二分+哈希?尝试用最朴素的方法(暴力)推导出最优解。
出题人视角:如果你是出题人,你会设置什么数据卡掉我的程序?这题的核心考点是什么?(是数学推导,还是代码实现?)
知识融合:将新学的算法与旧知识建立连接。比如学了“线段树合并”,回去看“并查集”和“可并堆”,思考它们的异同。
这一层复盘会让你逐渐形成“算法直觉”——看到题目关键词就能映射出可能的解法路径,就像老手看代码一眼就能嗅到性能瓶颈。
四、如何将复盘融入日常训练——像管理代码一样管理学习
4.1 建立你的“算法仓库”与“Bug追踪系统”
- 算法仓库:按专题分类,记录每种算法的核心思想、适用条件、模板代码、易错点
- Bug追踪系统:每次复盘发现的思维bug都记下来,打上标签(
[边界条件]、[复杂度误判]、[读题偏差])。每月review一次,看看高频错误是什么
4.2 为每次模拟赛写一份“调试报告”
cpp
/** * 模拟赛:2025.03.15 洛谷月赛 * 成绩:100+0+30=130 * * 主要问题: * 1. T2 爆零原因:数组开小 (RE) + 忘记开 long long (WA) * 2. T3 只拿部分分:只写了暴力,没时间想正解 * * 改进措施: * - 以后数组大小 = N+10,所有变量默认 long long * - 前 30 分钟快速浏览所有题,先写暴力再攻正解 */
4.3 坚持“补题”习惯
打完一场比赛后,一定要补题——继续做你没做出来的题。建议每次比赛在你没做出来的题中,选最简单的一两道题做会。实在不会就去看题解,但看完题解后,写一篇简短的题解,目的是为了让你把这道题再想一遍。
4.4 重视比赛中的“错误记录”
CSP认证允许将纸质材料带入考场。建议把训练中常犯的错误记录下来,考试中时不时看一下,提醒自己不要犯错。对于一些不太熟悉的算法与定理,也可以打印带入考场辅助解题。
五、结语:代码质量比数量更重要
cpp
// 错误的刷题观
for (int i=1; i<=3000; i++) {
solve_problem();
if (!review) continue; // 永远不复盘
// 能力提升?不存在的
}
// 正确的刷题观
while (true) {
solve_problem();
deep_review(); // 每次迭代都优化
ability += insight_gained;
if (ability >= PROVINCIAL_TEAM_LEVEL) break;
}
在C++的世界里,一段优雅、健壮、高效的代码,往往不是一蹴而就的,而是经过无数次调试、重构、优化才诞生的。你的能力也是如此。3000道题只是原材料,复盘才是那个将原材料锻造成利器的熔炉。
高效的学习比麻木地堆叠学习时间有效。在学习过程中,如果能抓住自己的弱项并加以训练,通常能得到更好的效果。代码能力较弱可以多找模拟题训练;思维与算法较弱而代码能力较强,则可以寻找对应的题目将思路思考清楚,而不一定要将代码写下来,从而节约时间并使训练更有针对性。
下次当你准备开启新一轮题海战术时,不妨先问自己:“我是想提交3000次WA,还是通过一次深度复盘,让下一次提交直接AC?”
从“Accepted”到“Provincial Team”,中间隔着的,正是那行被你遗忘的代码:
cpp
if (!review) continue; // 请务必把这行删掉
参考文献