临近复试的那几周,我几乎把所有空余时间都砸在了 OJ 上。要说最煎熬的,不是第一轮刷题时对着题目发呆的茫然,而是第二轮重刷时发现——当初 AC 过的题,现在连思路都想不起来。这篇复盘笔记,就是在这种被打击感里攒出来的。当时给自己定了个规矩:每完成一轮二刷,就写一篇复盘记录。现在轮到了第 11 篇,回头翻看这 11 篇的记录,能明显感觉到自己的思考方式在变,不是刷题量堆出来的那种“见过题”,而是面对陌生题时能更快地判断方向、规避陷阱。这篇文字主要围绕我在复试机试准备中的二刷复盘展开,讲一讲那些真正让我提分的经验:错题怎么重做才有价值、考点怎么分类优先级、限时做题时怎么做决策,以及最常见的那些低级错误到底怎么根治。给正在准备各类院校复试机试、保研机试,或者单纯想提升刷题效率的朋友做一个参考。
1. 为什么二刷比一刷更值钱——复盘这件事的核心逻辑
1.1 一刷是“看懂了”,二刷是“真的会了”
很多人在第一轮刷题时都有一个错觉:对着题解看懂了思路,调通了代码,就觉得自己掌握了这道题。但复试机试考的不是你看懂了多少题解,而是你在限时、高压、没有参考代码的条件下能不能独立写对。一刷时最常见的学习路径是:读题,没思路,打开题解,恍然大悟,照着敲一遍,AC,关掉。这套流程的缺陷在于——它跳过了最关键的一步:你自己从题目出发重建思路的过程。
二刷的价值恰恰在这里。当你隔一段时间再看同一道题时,记忆中的题解细节大多已经模糊了,必须靠自己对题目的理解重新推演一遍算法流程。这个“重新推演”的动作,才真正把知识固化成了能力。如果二刷时能顺利写出来,说明这道题已经内化;如果卡住了,那恰恰暴露了薄弱环节,值得深挖。我在复盘第 11 轮时明显发现,一刷阶段那些靠着“强行记忆模板”通过的题,到了二刷几乎全军覆没;而那些当时是自己一步步调试出来的题,隔了半个月再写依然顺畅。
1.2 复盘的四个层次,决定了你能走多远
刚一开始写复盘,我做的只是把题解粘贴进笔记,标个“易错”就算完事。后来才发现,这种复盘毫无意义,因为复盘的终点不是记录题目,而是归纳决策方式。我后来把复盘分成四个层次:
- 第一层:对着题解重新敲一遍代码,AC 就算结束。
- 第二层:不看任何参考,凭记忆独立重写,能写对说明基本掌握。
- 第三层:把同一类型的题目放在一起对比,总结它们的共同结构和解题模板。
- 第四层:抽象出“遇到某类特征怎么决策”的判断规则,比如“看到连通块就想到并查集、DFS、BFS 三选一”、“看到区间最值和子区间就想到滑动窗口或线段树”、“看到取余和模运算就警惕中间结果溢出”。
我坚持写了 11 轮复盘之后,最大的收获其实是第四层的东西——决策规则库。这玩意儿不是从某一道题里学来的,而是十几道甚至几十道相近题目的错题经验反复叠加出来的。一到考场上,时间紧张,思考会被压缩到很浅的层面,这时候能帮你的不是“我记得某某题怎么做”,而是“这种特征我应该优先考虑哪条技术路线”。复盘的层次越深,这条自动化的决策链就越可靠。
2. 复试机试的考点分布规律与优先级排序
2.1 大纲之外的潜台词:机试考的是基础功底
很多院校的复试通知里,对机试的描述就一行字“考察程序设计基础与算法能力”,但实际题目范围是可以预测的。对比几所院校近几年的复试机试题目,可以发现考点高度集中:模拟题、枚举与贪心、搜索(DFS/BFS)、图论基础(最短路、最小生成树、拓扑排序)、动态规划(背包、LIS、区间 DP)、常用数据结构(栈、队列、优先队列、哈希表)。为什么是这些?因为这些内容恰好能分割成可评分、可自动判题的“有标准答案”的问题,而且它们覆盖了一个计算机专业学生最核心的代码功底。
校赛 ACM 里那些复杂的网络流、平衡树、计算几何,在复试机试里几乎不会出现——不是题目不能出,而是复试考察的定位决定了它是“门槛测试”而非“选拔竞赛”。想明白这一点后,我的题单从“贪多求全”转向“核心考点反复练”,投入产出比立刻高了很多。
2.2 高频题型投入产出分析
我在第 11 轮复盘时整理了一张题型优先级表,结合自己的做题数据,标出了“必须全拿”和“可以适度放弃”的部分:
| 题型 | 出现频率 | 投入成本 | 易错程度 | 策略 |
|---|---|---|---|---|
| 模拟题(字符串处理、日期推算) | 很高 | 低 | 中等 | 必须系统练,细节决定成败 |
| 枚举与贪心 | 很高 | 低 | 低 | 优先拿稳 |
| DFS/BFS 搜索 | 很高 | 中 | 中等 | 必须熟练,用的频率极高 |
| 基础数据结构(栈、队列、堆) | 高 | 中 | 低 | 熟练 API 与底层实现切换 |
| 图论(最短路、最小生成树) | 中等 | 高 | 高 | 模板必须背熟,变体要理解 |
| 动态规划(背包、LIS、区间 DP) | 中等 | 高 | 高 | 重点吃透经典模型 |
| 复杂算法(网络流、后缀数组等) | 极低 | 极高 | 极高 | 不建议投入时间 |
以我复盘的统计来看,模拟和搜索加起来的占分往往接近一半,但很多人把时间花在钻研偏难怪题上,忽略了基本功。这是一个很典型的坑——在复试机试这个场景里,平易近人的基础题才是真正的得分主力。
3. 二刷实录:三个典型的“钉子户”错题拆解
3.1 图论变体:可以删除一条边的最短路
这次复盘中最让我头大的一道题,是在给定无向带权图上求起点到终点的最短路径,但允许你删除路径上的某一条边,删除后该边权值按 0 计算(或者等效为可免费通行一次)。第一眼看到这个题,我脑子里冒出来的想法是枚举删除哪条边,然后跑一次最短路算法。如果是朴素思路,删除每条边后都要跑一次,复杂度是 O(E·(V+E)logV),在图规模稍大的时候必然超时。
正确的做法是引入“分层图”的概念:把一个点拆成两个层次,第 0 层是还没使用“免费通行”的普通状态,第 1 层是已经使用了免费通行的状态。建边时,普通使用更新时同时向下一层连一条权值为 0 的边,代表“在通过这条边时使用了免费机会”。这样一来,问题就转化为在 2V 个节点上跑一次最短路,复杂度直接降回 O((V+E)logV)。二刷时我重新推导了一遍这个建图过程,发现之前 AC 不过是背了模板,并没有理解为什么能拆点、为什么边权为 0。这才是二刷真正该盯住的东西——算法不只是板子,而是看问题的视角。
3.2 动态规划:背包问题的“恰好装满”陷阱
动态规划里的状态初始化是一个反复栽跟头的地方。以 0/1 背包为例,如果题目要求的是“恰好装满背包的最大价值”,那么初始化时只有 dp[0] = 0,其余 dp[j] 应设为负无穷(比如 -0x3f3f3f3f),表示当前容量下“无法恰好装满”的非法状态。但如果题目只是“不超过容量”,则所有 dp[j] 初始化为 0 就可以。
复盘时我把自己之前的一个 WA 案例完整重建了:
#include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; int main() { int n, V; cin >> n >> V; vector<int> dp(V + 1, -INF); // 恰好装满 dp[0] = 0; for (int i = 0; i < n; i++) { int w, v; cin >> w >> v; for (int j = V; j >= w; j--) { if (dp[j - w] != -INF) { dp[j] = max(dp[j], dp[j - w] + v); } } } cout << (dp[V] == -INF ? 0 : dp[V]) << endl; return 0; }这段代码的关键在于“恰好装满”时,要先判断前一状态是否可达。这个判断漏掉的话,负无穷加上一个价值数反而变成了一个很大的负数,最终答案就错了。我在一刷时根本没想到这层,仅仅靠记忆“恰好装满用负无穷初始化”过了样例,但遇到需要进一步判断“是否真的可达”的题目变体时立刻暴露出理解不够的问题。
3.3 模拟题:被读题坑掉的分数
另一种容易反复错的题:模拟题。模拟题算法本身不难,难在准确复现题目的规则。我这次复盘里有一个很典型的例子——一道字符串处理题,规则里写明“输出数字之间用单个空格分隔,行末不得有空格”。样例数据长这样:
1 2 3样例的输出看起来没什么特别的,但用常规的“每输出一个数就追加一个空格”的方式,第三个数后面也会带上空格。在大多数 OJ 上,行末多余空格有时会被忽略,有时会导致 WA。遇到严格的判题系统,这就是一个隐藏罚分点。
这类问题的根治方式只有一个:输出时不要“边打印边加分隔符”,而是把要打印的内容收集起来,遍历前 N-1 个元素时打印“元素+空格”,最后一个元素单独打印并换行。这种细节第一次写的时候很容易忽略,复盘时把它单独列成一条“输出格式检查项”,之后再遇到格式要求复杂的模拟题,就能下意识地规避。
4. 限时机试下的算法选型决策——如何快速锁定正确解法
4.1 数据范围是最大的提示
说实话,我在一刷阶段拿到题目经常直接想用什么算法,结果复杂度估算错了导致超时。后来总结出经验:先看数据范围,再选算法模型。比如:当 n 在 20 左右时,可以放心用状态压缩或爆搜;n 在 10^3 量级时,O(n^2) 的动态规划往往可行;n 在 10^5 量级时,要么是 O(n log n) 排序、二分、优先队列,要么是线性扫描;如果 n 在 10^5 且计算过程涉及区间最大值最小值,那么线段树、树状数组、单调队列就是主要备选项。
我整理了一个“数据规模与可行算法”的速查表,每次做题前默念一遍:
| 数据范围 | 可用复杂度 | 常见算法示例 |
|---|---|---|
| n ≤ 20 | O(2^n) / O(n!) | 状态压缩 DP、回溯 |
| n ≤ 200 | O(n^3) | Floyd、区间 DP、三重循环 |
| n ≤ 10^3 | O(n^2) | 朴素 DP、双重循环枚举 |
| n ≤ 10^5 | O(n log n) | 排序、二分、STL 容器操作 |
| n ≤ 10^6 | O(n) | 前缀和、双指针、哈希表 |
| n 更大 | 低于 O(n),或需特殊优化 | 数学推导、贪心规律、矩阵快速幂 |
能看到这个表并不意味着直接套用,它的作用是帮你在拿到题目后迅速排除明显不可行的方案。我在二刷时有一个高频动作:看到题面后先把数据范围圈出来,判断枚举是否可行,如果不可行再考虑更优的数据结构或算法。这套刻意的练习做到后来基本不需要动笔,扫一眼范围就能定复杂度上限。
4.2 常数优化和代码风格也是隐藏得分点
限时机试还有一个经常被忽略的问题:同样复杂度的两份代码,常数差异可能让一个 AC,一个 TLE。比如大量数据输入输出时,cin/cout如果不禁用同步,速度会明显慢于scanf/printf,但用ios::sync_with_stdio(false); cin.tie(0);之后差距就几乎消失了。这个细节看起来是常识,但我在复盘时发现很多同学的提交就是因为这个被卡的。
此外还有一些常见的优化习惯:能用一维数组滚动更新就不要开二维;使用vector时提前reserve容量;频繁插入删除优先考虑list或直接数组模拟;图论题建议直接用链式前向星或邻接表,避免邻接矩阵的 O(n^2) 浪费。这些点单个看都不起眼,但在限时、数据范围压满的环境里,可能就是区分 AC 和 TLE 的那一根稻草。
5. 常见问题与排错技巧实录——那些让 AC 变成 WA 的元凶
5.1 五个最容易踩中的低级错误
复盘 11 轮下来,我把自己的报错原因做了统计,出现频率最高的是这五类:
- 数组越界:一写循环就让下标从 1 到 n,结果访问了 a[n+1] 或者 a[n] 在部分语言中已经越界,尤其在使用 1-index 与 0-index 混搭时最容易犯。
- 未初始化变量:定义了变量却不赋初值,在多次循环中累积了上一次的残留值,导致结果莫名错误。
- 边界条件遗漏:比如区间查询的左端点大于右端点、n 为 0 或 1 时的特殊情况、DFS 递归深度过大导致的栈溢出。
- 数据类型溢出:int 与 long long 混用,在累加较大数据时溢出,输出变成负数。这个问题在图论和动态规划里尤其普遍。
- 多组输入后容器未清空:写了
while(cin >> t),处理完一组后 vector、栈、队列里还留着上一组的数据。
我把这五类错误直接做成了一个“提交前自查清单”,每次提交前按顺序过一遍:
提示:提交之前,先检查边界数据(n=0、n=1、最大规模),再检查数组大小是否多开了至少 2 个单位的余量,最后检查所有累加量是否使用了足够的整数类型。这 30 秒能省下无数次的罚时。
5.2 调试三板斧:手算、对拍、打印中间变量
遇到奇怪的 WA,我经历过的有效排查流程有三步。第一步是构造极端小数据手算。比如 n=1、全 0、全 1、最小值、最大值这些场景,如果题目边界有问题,这一步基本能看出来。第二步是输出中间变量,在关键算法流程里加上cout << "dp[" << j << "] = " << dp[j] << endl;这类打印,和手算结果做比对,定位错误发生的具体位置。第三步是对拍,写一个随机数据生成器,再写一个朴素但正确性容易保证的慢速版本,和优化版本同时跑,比较输出是否一致。
对拍是我重点想推荐的工具。有一次我调一个二分答案的题,自己感觉算法没问题,但样例过了却 WA,最后生成了一万组随机数据,发现当mid取到某个特殊值时边界更新错了。这种隐藏很深的错误,靠肉眼盯代码很难找到,但数据多了就会自动暴露。二刷阶段我在每个专题后面都会顺手留一个对拍脚本,可以大幅提升调试效率。
6. 复盘节奏与考场心态——冲刺阶段的安排建议
6.1 复盘节奏:每周一套整卷加错题重做
很多人刷题是“刷完就扔”,但二刷的核心恰恰是要反复回访错题。复盘时我给自己拟了一个节奏:每周做一套完整的模拟题(按目标院校的时长限制),做完之后当天不对答案,隔一天后再做一遍错题,隔一周后再重做一遍。为什么要隔一天和隔一周?因为这样才能筛掉短期记忆的干扰。当天重做时往往还记着思路,隔天重做会有一部分遗忘,隔一周重做则基本只靠能力做题了。
在这个节奏下,第 11 轮复盘时我已经能明显感觉到哪些题是真正掌握的——那些隔一周还能快速写出并一次 AC 的题,基本不需要再看;那些隔一周依然卡壳的题,需要重新梳理思路,并额外寻找同类题巩固。还有个细节:复盘记录时不只写“这道题考了什么算法”,还要写“我为什么当时没想出来”“卡在了哪一步”“下次遇到类似题先往哪里想”。这些文字才是真正的经验资产。
6.2 考场上最实用的时间分配策略
最后聊一个很多经验帖不会细说的话题:考场上怎么分配时间。我的策略是拿到题目后先把整份试卷浏览一遍,快速给每道题标注难度等级:一眼有思路的直接标注为 A,需要想一想才能确定方案的标注为 B,完全没有头绪的标注为 C。然后按照 A → B → C 的顺序作答,先确保保底分数,再考虑攻坚。
对 A 类题,写完代码后立刻做边界测试,确认输出格式;对 B 类题,估算时间和复杂度后优先选择自己熟练的实现;对 C 类题,如果剩余时间充足,可以先尝试用最朴素的暴力方法拿部分分。这一套顺序的核心逻辑很简单——把必得分和可能得分区分开,不要在 C 类题上消耗过多的无效时间。每次模拟我都刻意练习这个流程,到真正复试时基本变成了本能反应。
复盘到第 11 次,让我最深的一个体会是:OJ 刷题这件事,决定上限的不是天赋,而是你多大程度上把每一道题的经验沉淀成了自己的东西。二刷的意义不只是重复做旧题,更是在时间间隔制造的“遗忘”里看到自己的薄弱环节,在错题分析里找到思维盲区。如果你也正在准备类似的机试,不妨给每道错题建立一张“病例卡”,记录症状、诊断结果和治疗方案。坚持一段时间后你会发现,真正能让你在考场上稳住心态的,不是刷了多少道题,而是你对自己的易错模式有多熟悉。这套方法不只适用于复试备考,对以后日常编码习惯的打磨也一样有好处。