距离东华复试还有十来天的时候,我的OJ二刷已经推进到了第六轮复盘。说实话,二刷这件事,一上来是真的枯燥——同一道题,明明一刷时已经AC过,再打开编辑器却经常发现“根本想不起来当时怎么过的”。但熬过第四轮之后,我开始感觉到了变化:很多题不再依赖脑内回放代码,而是能直接从题意推导出解法,边界条件也处理得更自然。这篇复盘记录了本轮训练里我认为最值得反复看的几类错误、三道代表性题目的完整推导,以及冲刺期的做题策略。如果你正在准备东华或者其他高校的复试OJ,处于“一刷刚结束、二刷找不到重点”的状态,这篇文章里记录的排查思路和刷题顺序,应该能帮你省掉不少瞎折腾的时间。
1. 东华复试OJ的“题感”:二刷到底在抓什么
1.1 复试机试和校招笔试真不是一回事
很多同学准备复试机试时,会直接照搬校招笔试那一套:狂刷算法题、背各种套路模板。但复试OJ其实更接近“限时编程基本功测试”,它不是要你发明新算法,而是看你在有限时间内能不能把基础算法写对、写干净、写稳。东华复试OJ的题目量一般不大,难度通常也低于ACM,但限时压力很真实,有时候一两个低级失误就能让你和录取线擦肩而过。
二刷和刷新题最大的区别也在这里:一刷的重点是“学会”,二刷的重点是“稳过”。同一个题型,一刷时AC了只能说明你当时的思路是对的;二刷时能快速写出AC代码,并且经得起特殊数据测试,才算真正掌握了。所以我在二刷阶段不再追求题量的堆积,而是把大量时间花在“拆解错误”和“重做旧题”上。
1.2 高频题型优先级:我如何重排刷题顺序
二刷开始前,我先把东华复试常见题型按优先级做了个排序,避免把时间花在低概率考点上。下面是我整理的一份参考表,不同年份、不同学校题目风格可能略有差异,但总体思路是通用的。
| 优先级 | 题型 | 理由 | 建议投入 |
|---|---|---|---|
| 高 | 输入输出、数组/字符串模拟、排序、二分、BFS/DFS、简单DP | 复试几乎必考,覆盖大多数中等题 | 每天固定训练 |
| 中 | 链表操作、栈/队列、图的最短路、最小生成树、拓扑排序 | 东华这类学校复试有概率出,属于常见第二题/附加题 | 隔天练1-2道 |
| 低 | 网络流、计算几何、FFT、平衡树 | 复试机试基本不会考 | 直接放弃 |
一刷时我花了很多时间在“中”优先级题型上,导致基础模拟题手速不够。二刷时我明显把重心调了回来:每天前半小时做输入输出和字符串模拟,再花一小时做搜索和DP,最后的碎片时间才碰图论。这样调整之后,做题稳定性和手速都提升得很明显。
1.3 我给自己的“二刷及格线”
二刷不能漫无目的地“再做一遍”,我给自己定了三条量化标准:
- 简单题(模拟、输入输出、排序):5分钟内读题并动笔,10分钟内AC。
- 中等题(搜索、简单DP、图论基础):15分钟内形成思路,30分钟内写出可提交代码。
- 任何一道题如果卡了20分钟没有进展,立刻标记为“待复盘”,不硬耗。
这个及格线看起来很功利,但复试机试就是一场限时赛。平时的训练如果总是纵容自己“再想一会儿”,到了考场上就会变成“再耗一会儿”。二刷的价值就在于把做题节奏练成肌肉记忆,而不是真的去挑战高难度题。
2. 这次二刷最扎心的三类低级失误
2.1 边界值不是想不到,是想到了又忘了
这次复盘中,我又一次在一道矩阵旋转题上栽了跟头。一刷时我就犯过错:把m和n写反。按理说这种错误已经记录过了,但二刷重写的时候,注意力全放在旋转公式上,结果输入部分读取还是先n后m,导致测试数据一出现行数不等于列数的情况就数组越界。
边界值处理,最可靠的办法不是“做题时想一想”,而是在读完输入之后立刻处理。比如矩阵题,在读取完m, n后马上检查是否m <= 0 || n <= 0,并且把行、列变量名统一成rows、cols,不要用单个字母换来换去。
还有一个很实用的习惯:如果题目描述里给了数据范围,就在代码注释里写上“n=1”和“n=最大值”这两个极端样例。等到代码写完准备提交前,先跑这两个用例。这一步能过滤掉很多隐藏的越界和死循环。
2.2 输入读取:差一个循环带来的灾难
复试OJ的输入格式通常有三种:单组数据,第一行给测试组数T的多组数据,以及读到文件末尾(EOF)为止的循环输入。二刷时我一度看到while(cin >> n)就直接写,结果有一道题其实是先给T,再用T控制循环次数,最后程序完全跑偏。
更常见的问题是混合使用cin和getline。比如题目先读一个整数n,然后读n行字符串,如果直接cin >> n; getline(cin, str);,第一行读到的往往是空字符串,因为换行符还残留在输入流里。我现在的做法是:如果题目出现字符串含空格的输入,就统一用getline,并且在读整数后手动加一个getline吞掉换行。
处理多组输入时,我给自己总结了一句口诀:先确定循环条件,再确定每组数据的初始化位置。把变量定义尽量放在循环内部,避免上一组数据残留影响下一组。这个细节虽然简单,但二刷时依然救了我好几次。
2.3 输出格式:评测机只认精确字符串
这道坎我踩过很多次,这次二刷又踩了一脚。题目要求输出的每个数字之间用单个空格分隔,行末不能有多余空格。很多同学(包括我之前的习惯)会写:
for (int i = 0; i < n; i++) { cout << a[i] << " "; }这样完全AC只能靠运气,因为行末会多一个空格。严格OJ会直接判WA。
我现在改成用变量控制分隔符:
for (int i = 0; i < n; i++) { if (i) cout << " "; cout << a[i]; }输出格式里的大小写、Case #x:前缀、末尾换行、多组数据之间是否需要空行,这些都要在读题时圈出来。我建议先把这些细节记录在草稿纸上,而不是靠脑记。因为考场上越紧张,越容易把平台要求的格式输出错。
2.4 变量初始化:静态全局数组的“安全感”假象
有相当一部分同学喜欢把数组开到全局,认为这样默认初始化为0比较“安全”。这个习惯本身没问题,但如果一个程序里有多组测试数据,而组与组之间没有重置数组,就会出现“上一组数据污染下一组结果”的经典错误。
二刷时有一道统计字符串中字母出现次数的题,我全局定义了一个cnt[26],统计完一组输出后忘了memset,导致第二组数据的结果会把第一次的计数叠加进去。排查了很久才发现:第一组数据恰好让某些字母为0,结果看起来像是对的,可数据一换就错。
解决思路很简单:给多组数据的题目写代码时,循环体内使用的计数数组、状态数组、标记数组,一律在每组循环开头初始化。可以就用fill或memset,明确重置过总比指望“默认0”更可靠。
3. 三道代表性题目的推导与代码取舍
二刷时遇到的题目不见得都是原题,但题型可以归纳成几类。这里选三道我本轮印象最深的经典题型,完整走一遍推导过程。
3.1 最长上升子序列:从O(n²)到O(nlogn)
题目描述通常很直接:给定长度为n的数组,求最长严格上升子序列的长度。一刷时很多同学会先写O(n²)的DP,这没错,但复试的数据范围如果到10⁵,O(n²)就会超时。
O(n²)的思路是设dp[i]表示以第i个元素结尾的最长上升子序列长度,转移时需要枚举j<i。而O(nlogn)的做法维护一个tail数组,tail[k]表示长度为k+1的上升子序列中末尾元素的最小值。遍历每个数时,用lower_bound找到第一个不小于它的位置,替换掉该位置的末尾值。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n; cin >> n; vector<int> a(n); for (int i = 0; i < n; i++) cin >> a[i]; vector<int> tail; for (int x : a) { auto it = lower_bound(tail.begin(), tail.end(), x); if (it == tail.end()) tail.push_back(x); else *it = x; } cout << tail.size() << endl; return 0; }这里有一个特别容易错的点:题目要求“严格上升”,所以要找lower_bound;如果要求“非递减”,需要改用upper_bound,保证相等元素也能接在末尾。二刷时我差点在这个细节上翻车,因为一刷时写的是非严格版本,这次没仔细读题。
复试现场建议先用5秒判断数据范围:如果n <= 5000,O(n²)的DP也可以快速写完保底;如果n明显更大,直接上O(nlogn)版本。两者都需要掌握,因为不能保证现场运气一直好。
3.2 二叉树的层次遍历:队列写法的两个关键点
二叉树层次遍历本身不难,但复试常把它包装成“根据先序序列建树,再输出层次遍历结果”。也就是说,核心考点有两个:一是建树,二是层序遍历。
一个常见的先序序列输入会以#表示空节点。建树用递归:
struct Node { char val; Node *left, *right; Node(char v) : val(v), left(nullptr), right(nullptr) {} }; Node* buildTree(const string& s, int& idx) { if (idx >= s.size() || s[idx] == '#') return nullptr; Node* root = new Node(s[idx]); idx++; root->left = buildTree(s, idx); idx++; root->right = buildTree(s, idx); return root; }层次遍历用队列:
#include <queue> void levelOrder(Node* root) { if (!root) return; queue<Node*> q; q.push(root); bool first = true; while (!q.empty()) { Node* cur = q.front(); q.pop(); if (!first) cout << " "; first = false; cout << cur->val; if (cur->left) q.push(cur->left); if (cur->right) q.push(cur->right); } }这里最需要注意的是字符串下标的推进。建树函数中,idx++的位置必须严格控制:每次递归处理完当前节点后,要让下标指向右子树的起始位置。如果idx没有正确更新,要么死循环,要么建出来的树完全错乱。我在二刷时就因为少写了一个idx++,导致样例输出和预期完全不同,调试了十几分钟才发现原因是空节点被连续跳过。
复试现场遇到树题,我优先选队列版本,因为迭代写法不容易爆栈,而且逻辑更贴近层次遍历的定义。如果你对指针操作不熟,也可以用数组模拟二叉树节点,但思路完全相同。
3.3 拓扑排序判环:入度表维护的细节
有一类高频图论题是这样的:给定n个任务和m组依赖关系,问能否按照依赖顺序完成所有任务。这本质上就是判断有向图是否存在拓扑序列,或者说是否含环。
Kahn算法的核心是维护每个节点的入度,把所有入度为0的节点放入队列,逐个弹出后,将邻接点的入度减1,再次把入度变为0的节点入队。如果最终出队节点数量等于n,说明存在拓扑序;否则有环。
#include <iostream> #include <vector> #include <queue> #define MAXN 1005 using namespace std; vector<int> edge[MAXN]; int indeg[MAXN]; bool topo(int n) { queue<int> q; for (int i = 1; i <= n; i++) { if (indeg[i] == 0) q.push(i); } int cnt = 0; while (!q.empty()) { int u = q.front(); q.pop(); cnt++; for (int v : edge[u]) { indeg[v]--; if (indeg[v] == 0) q.push(v); } } return cnt == n; }上面这段代码本身很简单,但二刷时我在“重边”上踩了坑。如果输入里有两条相同的依赖边a -> b,有些同学在构造图时可能默认去重,但如果没有去重,indeg[b]会被加了两次,导致b永远无法入队。更稳妥的做法是,先读完全部边,再用set或标记数组去掉重复边,再统计入度。
另外,edge和indeg在多组数据时必须在每组开头重置。我习惯把这两个数组定义在topo内部,或者每次调用前统一clear。这个点看似是代码洁癖,但在考场上能直接决定你是否能一次AC。
复试中图论题出现频率不算特别高,但拓扑排序是“既基础又容易包装成实际问题”的考点,二刷阶段值得花一晚上把Kahn算法写熟,并搞清楚它和DFS判断环的区别。
4. 一刷和二刷的差异:错题本应该怎么用
4.1 我的错题本结构
一刷时我习惯把所有WA的题解都截图保存,结果复盘时连自己错在哪都找不到了。二刷我改成结构化错题本,每道错题只记四栏:
| 日期 | 题型/题目特征 | 错误类型 | 一句话总结 |
|---|---|---|---|
| 第6轮 | 矩阵旋转 | 行列搞混 | 统一用rows/cols变量名,读入后先查边界 |
| 第6轮 | 字母计数 | 组间数据污染 | 多组数据循环内显式重置计数数组 |
| 第6轮 | 拓扑排序 | 重边导致入度错误 | 构图前去重或读边时同步去重 |
这里关键的是最后一栏“一句话总结”。能用一个短句写明白的错误原因,说明你真的理解了;如果只能写“代码WA、检查后发现数组越界”这种模糊总结,那下次大概率还会犯。
4.2 二刷时如何判断自己“真会了”
我判断一道题是否需要“三刷”,标准很简单:能不能在不看任何代码的情况下,给一个完全没做过这道题的同学讲清楚思路。如果讲的时候还需要翻题解,说明这题还没变成自己的东西。
复试机试和期末考试不一样,它不允许你临时翻笔记。所以二刷目标就是让一道题变成“本能反应”。每次重做旧题时,我会先逼自己在10分钟内手写一遍核心代码,然后再对照之前的错题总结。如果两次写法一致且AC,就在错题本上划掉这道题;如果有新的理解,就追加一句新的总结。
4.3 时间分配和心态:不要连续三天只刷同一类题
二刷阶段我在心态上走过一段弯路:连续三个晚上只做动态规划,做到第四天看到“状态转移”四个字就生理性恶心,做题正确率也明显下降。
后来我把训练内容打散,每天规定“1道输入输出/模拟 + 1道搜索或DP + 1道图论或树”,有时候还会穿插字符串处理。这样每种题型都能保持手温,又不会让大脑在同一个思维模式里疲劳。坚持几天后,整体手感比集中刷一类题要稳得多。
4.4 复盘到了第六篇,我开始做减法
前五篇复盘我写得很细,甚至会把某段代码一行一行贴出来。到了第六篇,我发现真正值得写进复盘的,反而只有“错误类型”和“效率瓶颈”。刷题数量上去后,每天真正全新的失误点其实不会超过三个,大量WA是旧错误的变体。
所以本篇复盘也在刻意做减法:不再堆砌所有做过的题,只记录三类典型失误和三道代表性题目。如果你的二刷已经进入中后期,我也建议你把错题本里的“水分”挤掉,只留下那些能在冲刺期快速提醒自己的核心要点。
5. 复试前的冲刺安排:把二刷成果转化成考场手感
5.1 最后三天的全真模拟
二刷到了这个阶段,不应该再漫无目的地刷题。我会在正式复试前三天,每天安排一次完整的机试模拟。选一套题型和难度都比较接近复试的模拟题,设定和真实考试一样的时长,然后关闭所有笔记和题解,像真正考试一样从头做到尾。
模拟时要注意两个细节:第一,不要中途停下去查资料,哪怕卡住了也要按考场规则来;第二,记录每个题的第一遍提交结果,而不是“反正模拟嘛,可以多交几次”。模拟的目的就是暴露真实水平,多交几次只会掩盖问题。
5.2 考场上的时间止损顺序
复试机试时间一般比较紧,我先读完全部题目,再按以下顺序执行:
- 题面最短、输入输出最简单的题,力争5分钟内AC,这是拿分基本盘。
- 自己最擅长的算法题,比如熟悉的DP或搜索,稳定拿分。
- 剩余题目按“思路清晰分值高”来排序,啃得动就写,啃不动就及时收手,绝不恋战。
这个顺序的核心思路是,先把确定的分拿满,再用剩余时间冲击难题。很多同学喜欢一上来就啃第一题最难的那道,结果第一题卡半小时,后面送分题没时间写,这是复试机试最可惜的失败方式。
5.3 东华OJ平台操作的小细节
最后一天我还会做一件小事:把所有参加复试会用到的代码模板重新敲一遍。比如快读模板、建树模板、拓扑排序模板,不复制不粘贴,手动输出一遍,确保考场上不会因为忘记头文件这种理由浪费心理资源。
提交语言选对也很关键。东华OJ支持C++的话,我保险起见会用标准头文件<iostream>、<vector>、<algorithm>等,而不是<bits/stdc++.h>,因为不同OJ对万能头文件的支持不一样。另外main函数一定是int类型,并且返回0,这是最基本的AC条件。
还有一点容易被忽略:很多OJ对输出最后一个空格的处理很严格,对换行的要求却没那么统一。有的题目说“每组数据输出后跟一个空行”,有的说“每组数据间用一个空行隔开”。读题时看到这类词,在草稿纸上画一个输出效果示例,能有效避免理解偏差。
整个二刷过程让我最受益的,其实不是AC数量变多了,而是面对WA的时候不再慌。以前一看到Wrong Answer就怀疑自己算法不行,现在会先检查输入输出格式,再检查边界条件,最后才回头看核心逻辑——这个排查顺序本身就帮我省掉了大量无用功。
复试前的最后几天,比起追难题,我更建议你把过去复盘中所有“低级错误”快速过一遍。这些内容不会被写进高分经验帖,但恰恰是考场上最不值、却最可惜的扣分点。二刷的意义说到底就一句话:把“会做”变成“稳做”,把“碰运气”变成“确定性”。希望这篇复盘能让你少走一点弯路,也希望大家都能稳稳上岸。