
“3.4复试编程题五道”——这是我硬盘里一个文件夹的名字。当年准备研究生复试的时候,我给自己定了一组训练单元:四十五分钟,五道编程题,覆盖输入输出、数据结构、算法思想、模拟题和开放性设计,全部在OJ上跑通才算过。后来我发现,这五道题背后几乎压中了复试上机环节的所有高频考查面。复试编程题和初试的纸上写代码完全不是一个物种。初试看重语法正确性和逻辑完整度,复试上机则看你在真实评测环境里的代码成型速度、边界敏感度、以及十分钟内能否从题目文本提炼出可运行的方案。很多初试高分的同学恰恰栽在这里——不是不会写,而是没在OJ环境里写过完整程序。这篇文章我会把这五道题从题型设计到解题模板、从时间分配到避坑清单完整拆开讲,适合正在准备计算机类考研复试的同学,也适合想系统训练上机节奏的读者跟着走一遍。1. 复试编程题到底在考什么:五道题背后的考核逻辑1.1 为什么复试要专门设置编程题环节研究生复试单独搞一个编程环节,核心目的是验证三件初试看不出来的事:第一,你的代码能不能在别人给定的环境里跑起来;第二,你的代码能不能在边界情况下依然稳定;第三,你面对陌生题目时,第一时间的建模能力怎么样。初试手写代码考的是“知道怎么写”,复试上机考的是“真的写出来并能够通过测试集”。这里的差距,恰恰是很多初试高分选手翻车的重灾区。举个我印象很深的例子:有个同学初试数据结构接近满分,复试上机第一题是“读入一个整数矩阵,输出它的转置”。他在草稿纸上写得非常漂亮,但到了OJ上,第一次提交因为忽略多组输入直接超时,第二次因为读入循环里没处理换行符导致多读了一个空行。这已经和算法能力无关了,纯粹是对在线评测环境的陌生。从导师的角度看,上机环节的信息量远大于笔试。代码里能看出你有没有独立写过完整程序,有没有处理过真实数据,甚至能看出你的代码风格和工程习惯。复试编程题的本质,就是在用压力场景筛掉那些“只会在纸上编程”的人。你要是能稳稳写完并通过测试,导师就会觉得这个学生进了实验室能直接上手写代码,带起来省力得多。1.2 五道题的结构设计规律复试的编程题很少出偏题怪题,反而难度梯度设计得很讲究。我根据自己的备考经验,总结了下面这个高频难度分布:题号常见类型难度段位考查重点理想耗时第一题基础输入输出/简单计算送分题语法熟练度、输入输出处理5分钟第二题字符串/数组操作基础题边界判断、常用库函数10分钟第三题链表/栈/队列核心题数据结构掌握程度15分钟第四题模拟/状态机压轴题代码组织能力、调试能力20分钟第五题算法优化/开放设计拔高题复杂度意识、迁移能力15分钟第一道题基本是送分题,用来稳住考场心态;第二、第三道是核心区分题,难度适中但埋着边界条件的坑;第四道偏模拟,代码量大,考工程实现能力;第五道往往比较开放,考你知识迁移和设计思维。还要记住一点:复试OJ通常按测试点独立计分,部分通过也有分。所以整体策略不是每道题都完美,而是尽量多拿分。我在备考时给自己定的主线是“先拿稳分、再啃难题”,这个策略后面具体展开。2. 五道真题拆解:从题目类型到解题模板2.1 第一类:输入输出与基础语法题这类题长这样:读入若干整数求和、判断闰年、计算斐波那契数列前N项、反转字符串。听着简单,但复试上机里挂人的恰恰是送分题,原因几乎一模一样——不是不会算法,而是对输入格式不敏感。常见陷阱有三个。第一个是多组输入。题目写“输入包含多组测试数据,每组占一行”,很多人只写单组逻辑就提交,结果只拿到一两组测试点的分。处理很简单:C/C用while(scanf(%d, n) ! EOF),Python用for line in sys.stdin,核心是你要有“默认考虑多组输入”的意识。第二个是空行和多余空格,读字符串时要不要跳过空格、要不要去掉换行符,不同OJ差异很大,考前务必用一道题摸清目标OJ的读入习惯。第三个是数据类型溢出,斐波那契第50项就已经超过int范围,需要提前判断用long long还是写高精度。这份模板建议直接背下来:C语言环境下,带多组输入的整数处理统一用while(scanf(...) ! EOF);Python环境下统一用for line in sys.stdin,字符串读入注意strip()。这套框架一旦固定,送分题几乎不会失手。我当时给自己定了规矩:第一道题无论多简单,也要先在本地跑三组样例再提交。一道送分题的失误代价太大,可能直接把排名拉开好几位。2.2 第二类:核心数据结构题复试题里出现频率最高的是数组、字符串、链表、栈和队列。它们本身不难,但上机题特别喜欢做一层包装,让你先建模再动手。典型题目包括“判断括号是否匹配”“合并两个有序链表”“用两个栈实现队列”“找出数组中出现次数超过一半的元素”。做这类题,我建议固化成一个标准流程。第一步不急着写代码,先在草稿纸上把数据结构画出来:链表指针怎么指、栈怎么进出、队列顺序怎么保证;第二步专门想空数据、单元素、头尾节点这些特殊情况;第三步才动手写。很多同学拿到题就敲键盘,链表题一上来就处理错了头节点,然后陷入漫长的调试。先在纸上画三十秒,能省下十分钟。以“合并两个有序链表”为例,真正拉开差距的不是迭代逻辑,而是头节点处理。我推荐统一使用哑节点技巧:dummy new ListNode(0),用一个cur指针在dummy上移动,最后返回dummy-next。这样代码不用单独判断哪个链表为空,也不用特判头节点,是我写过最稳的写法。“用两个栈实现队列”也是同样的道理,只要坚持“pop时若输出栈为空,就把输入栈全部压入输出栈”这一条原则,代码结构就是固定的。数据结构题考查的不是灵光一闪,而是你脑子里有没有缓存这些成熟套路。2.3 第三类:算法思维题第三类题偏向动态规划、贪心、二分、DFS/BFS。难度到不了竞赛级别,但绝对能区分“背过模板”和“真懂原理”的人。典型题有最长公共子序列(LCS)、零钱兑换最少硬币数、岛屿数量、01背包简化版等。我在这类题上踩过一次很深的坑。当时写“零钱兑换最少硬币数”,一看是典型DP,立刻开写:dp[i]表示凑到金额i需要的最少硬币数,外层循环金额、内层循环硬币面值。样例全过,提交只拿一半分。排查半天发现题目里有一组数据的面值包含0,导致内层循环出现了“永远选不完”的情况,运行时直接异常。DP框架谁都会写,但“面值为0”“目标金额为0”“硬币种类为空”这些边界才是上机题真正埋雷的位置。所以我建议写DP前先把状态定义想透:状态转移不能凭感觉写,必须想清楚“当前状态依赖哪些先前状态”或“从当前状态能推出哪些后续状态”。方向定了,递推顺序自然就出来了。另外,能滚动数组优化的尽量滚动数组优化,因为复试OJ的内存限制有时候卡得很死,一个二维dp数组在数据范围稍大时可能直接MLE。不需要背太多难题,但LCS、背包、岛屿连通块这三个基础算法,一定要练到闭眼能写的程度。2.4 第四类:模拟题与综合应用模拟题是复试上机的分水岭。它不考高深算法,考的是“把一个过程老老实实用代码描述出来”的能力。典型题目有简单计算器表达式求值、约瑟夫环、LRU缓存模拟、小游戏状态演化、报表统计。这类题最大的挑战不是思路,而是代码结构能不能支撑起足够复杂的状态。我见过太多人把所有逻辑堆进一个大循环,中间塞七八个if,一旦某个状态没考虑全,整个程序行为就失控,而且极难排查。更稳的做法是拆函数:读入阶段一个函数、状态更新一个函数、输出阶段一个函数,每一条规则单独封装。代码量看着多一些,但每条规则都能独立测试,定位错误会快很多。以LRU缓存模拟为例,你需要同时维护哈希表和双向链表,get和put各有好几条分支。如果全塞在main里,光是把“链表断开重接”的指针操作写对就很难受。我当时先写removeNode函数,再写addToHead函数,最后get和put只是几条分支调用它们,整个过程思路清爽,调试几乎没费劲。模拟题写得好不好,本质上是工程能力的体现,而工程能力恰恰是导师在复试里最想看到的东西。记住一句话:模拟题宁可多写十行清晰代码,也不要少写三行跑飞的逻辑。2.5 第五类:扩展开放题最后一道题往往不是标准OJ题,而是开放性的设计题或优化题。比如“给一个栈结构,设计一个能在O(1)时间获取最小值的类”“把一个递归改写为迭代”“手写一个字符串匹配函数并分析复杂度”。这类题目没有唯一正确答案,关键看你能否分析复杂度并给出合理的工程取舍。应对开放题,要养成“先说思路再写码”的习惯。以最小栈为例,核心是额外维护一个辅助栈,每次入栈时把当前最小值也压进去,出栈时同步弹出,这样getMin就是O(1)。题目本身不算难,但面试官更关注你能不能主动提到“空间复杂度是O(n),可以只在最小值变化时入栈来优化”。如果能再补充“数据量极大时,可以用两个变量记录次小值来压榨空间”这类延伸思路,即使不实现,也会明显加分。再比如手写字符串匹配,最朴素的写法是双重循环O(n*m)。你能写出KMP并解释清楚next数组的构建过程,基本就是满分。可要是没把握把KMP写对,我建议诚实写朴素版本,再用几句话分析它在大多数场景下为什么够用,而不是硬着头皮写残缺的KMP。复试老师最反感不懂装懂,你愿意展示思考过程,反而比假装全会更容易拿印象分。3. 上机实操:五道题从读题到通过的全流程战术3.1 时间分配与答题顺序策略我一直用“先易后难、稳拿基础分”的策略,这和初试做题顺序一样,但上机有一个独有特点:每道题通常有多个测试点,部分通过也有分。所以开考后的前两分钟,应该把五道题全部扫一遍,给每道题标上“会做/勉强能写/完全没思路”的等级,再预估代码量。我的时间比例大概是:送分题5分钟,两道基础题各10分钟,核心算法题15分钟,模拟题20分钟。如果某道题在预估时间内卡住超过十分钟,立刻标记跳过,先做后面会的题。上机考试最忌讳的就是在一道题上产生“不甘心”——你不会因为一道题没做出来而挂掉,但会因为把后面题目的时间全部耗光而挂掉。学会止损,是上机最重要的应试能力之一。我还习惯把“留出15分钟检查”写进计划。检查不是重新读代码,而是针对性地测边界:输入为空、数据量最大、单元素、重复元素、负数、极端大数。这些用例在本地跑通后,整体通过率会有肉眼可见的提升。往年很多同学卡在“样例全对、提交只有一半分”,八成就是边界测少了。3.2 写码节奏:先骨架后细节拿到一道题不要马上写细节。我会花三十秒到一分钟,在草稿纸上写下三样东西:输入是什么、输出是什么、核心步骤分哪几块。这个过程看起来“浪费”时间,实际上帮你在脑子里把整个程序先跑了一遍,后面写起来更快,也更不容易写乱。举个例子,第三题如果是“读入m行n列字符矩阵,统计由‘#’组成的连通块个数”,我第一步会在草稿上写三行:一是读入矩阵,二是BFS/DFS标记连通块,三是输出结果。然后确认一个问题:矩阵的行列谁是m谁是n,别搞反;还要确认visited数组是复制一份还是原地改字符。这些细节,就是OJ上“通过”和“编译失败”的区别。写码时我坚持几个固定习惯:所有变量在使用前先声明并初始化,不在for循环里顺手声明容易误导的变量;所有函数在main前先写好声明;每写完一个函数就本地编译一次,而不是等全部写完再编译。这个“增量编译”习惯真的能救命。复试上机环境里,编译错误是最蠢的丢分方式,而增量编译能把错误范围缩小到最近改动的十几行之内,排查速度完全不一样。3.3 本地自测与边界样例的构造自测不是把题目给的样例跑一遍就完事。题目样例通常是最温和的路径,真正考验人的是“题目没明说但你迟早会撞上”的用例。我给自己整理过一份万能样例清单:空输入、单元素、最大数据量、最小数据量、全相同元素、全不相同元素、负数、0、极大值、极小值。以“求数组连续子数组最大和”为例,很多人的标准写法能过样例,但遇到全负数数组就错,因为经典的贪心写法遇到负数累加时直接重置,而正确答案应该是返回最大的那个负数。如果你自测时专门放一个全负数的用例,这个问题当场就会暴露,而不是等提交后对着WA发愣。上机OJ多数不会告诉你具体错哪个输入,只会告诉你哪个测试点错了,所以自测能力有多强,基本就等于你的真实得分上限。还有一个很关键的原则:所有测试不要只在脑子里跑。人脑跑程序会不自觉按“代码期望的行为”去推断,很难发现真实错误。哪怕是一个很小的样例,也要在本地终端里实际运行,看输出是否真的符合预期。复试上机的高手和普通选手,有时候就差这一步:普通人样例过了赶紧提交,高手会多花一分钟构造一两个刁钻用例,确保交出去的每一题都牢靠。4. 高频踩坑与排查实录4.1 编译环境与提交格式问题复试上机在不同学校差异很大,有的用老版本GCC,有的用Visual Studio,有的只提供Python 2,有的OJ对Python版本卡得很死。我身边最冤的例子是:同学用C11的auto遍历map,本机VS编译通过,结果学校OJ的GCC版本旧,直接编译失败,一道题零分。所以考前务必查清楚目标院校的编译器版本和语言标准。查不到的话,就用最保守的写法:不用auto、不用C11特性、不用尾随返回类型,这对拿分来说最稳。提交格式也是重灾区。有的OJ要求提交整个文件,有的只要求提交类和函数实现,有的要求输出精确到小数点后几位。关于输出格式,我的办法是:先跑一个最小样例,用肉眼对比输出与题目示例在空格、换行、大小写、小数点位数上是否完全一致。一个多余的逗号、一个末尾空格、一个错误的换行符,都可能造成Presentation Error甚至Wrong Answer。这些差异本地根本发现不了,只能靠你去适应OJ的严格规则。4.2 边界条件与数据范围复试编程题最喜欢埋雷的地方,第一是数组越界,第二是数据溢出,第三是特殊输入。数组越界的典型场景是DP递推里访问dp[i-1]时i从0开始,解决办法是给dp数组多开一行一列做padding;数据溢出的典型场景是int乘int,比如两个10^9量级的数相乘直接溢出成负数,解决办法是直接用long long;特殊输入则是“输入可能含重复元素”“矩阵可能不是正方形”“图可能不连通”这类表述,看到就要多留个心眼。我自己的排查顺序是固定的:先查所有for循环边界,再查所有减法操作,再查所有数组和vector的下标,最后查类型转换。这个顺序能覆盖我犯过的绝大多数错误。每次上机训练结束,我会把当天的“病历本”记一下,给错误分个类,考前翻一遍,效果比临时刷十道题都好。边界条件这种东西,只有靠你自己踩过、记过,才会形成条件反射。4.3 面试官追问时的应对思路复试编程环节有时候不止上机,还会有面试官在旁边看你写代码,或者写完后追问设计思路。高频问题就三个:“时间复杂度是多少”“为什么选这种数据结构”“能不能再优化一下”。这些问题的答案,应该在你动手前就想清楚,而不是写完了临时现场编。我的应对模板很简单:先说思路,再说复杂度,最后权衡。比如最大值连续子数组这道题,可以这样回答:状态定义dp[i]表示以第i个元素结尾的最大子数组和,转移方程是dp[i] max(nums[i], dp[i-1] nums[i]),时间复杂度O(n),空间复杂度可以压缩到O(1)。这种回答从定义、转移、复杂度三个维度完整展示思考链路,即使代码有小瑕疵,面试官也会觉得你是真正理解这道题的人。反过来,如果只说“我就是按样例硬写的”,即使AC了,印象分也会差一截。另一个加分的做法是主动讲清楚“我在这个题上做了哪些边界处理”。面试官最想听到的是“我考虑了数组为空”“我考虑了只有单元素”“我把负数情况单独测过了”这类话。它证明你的代码不是碰巧AC,而是有过系统性思考。复试这种高压场景里,这种“可交流的编程素养”往往就是导师决定要不要你的关键分。5. 备考资源与日常训练方法5.1 常见OJ平台与刷题策略复试备考不需要追求刷题量,但一定要保持手感。我推荐在力扣、牛客以及一些经典OJ上按专题刷,重点覆盖数组、字符串、链表、栈、队列、二叉树和图的基础算法。每天保持三到五道题的节奏就够,关键是每道题都要用一门主语言完整写出来并提交通过,而不是看一眼思路就划走。刷题策略上,我要特别强调“错题重做”的价值。很多人刷题数量上去了,正确率没上去,因为做错的题从没回头重新实现。我备考时建了一个表格,记录每天做了哪些题、错误类型、通过状态、复盘一句话,每周日只做表里标记过“WA”的题目,直到它们全部变成通过状态。这套流程坚持一个月,对边界条件的敏感度提升是立竿见影的。除了刷题,我会特意练一门主语言的IO处理。不同语言在OJ上的待遇完全不同:Python写算法题快,但输出格式控制要小心;Java和C运行表现稳定,编译调试成本相对低;C语言在老OJ上兼容性最好,但标准库少,写链表要手动管理内存。我的建议是认准一门最熟的语言,把输入输出、常用数据结构、排序与遍历能力练到不用查资料的程度。不要三心二意同时练三门,那样每门都半生不熟。5.2 从真题到变体的训练法复试题有个很有趣的现象:同一个知识点,换个包装就变成新题。比如“求数组中出现次数超过一半的元素”,可以包装成“投票算法的工程化场景”,也可以包装成“无序流数据中的众数问题”。这提醒我,备考不是背某道题的答案,而是从一道题提炼一类题的解法模板,再把模板套到三四个变体上。我的训练方法是:每做完一道题,强迫自己再想两个变体。原题是“给定数组,移除指定值”,变体一是“移除所有重复值并保持相对顺序”,变体二是“原地移除使元素全部唯一”。两个变体都用同一份双指针思路去推,推的过程就是真正的知识迁移。比起不断刷新题,这种“一题三练”在复试场景下性价比更高,因为复试题目总量有限,你几乎肯定能碰到眼熟的题型。还有一个容易被忽略的训练维度:手写完整代码,而不是只写核心函数。复试上机的代码量一般在百行上下,包含完整读入、输出、异常处理。平时如果只练函数体,到了现场往往会在“如何读入一整行带空格的字符串”“如何输出带两位小数的浮点数”“如何申请二维数组”这类基础环节卡壳。这些细节必须在平时训练里覆盖,不能指望现场临时想起来。5.3 编程题之外的隐性加分项最后说一个容易被忽视的点:编程环节除了看代码过不过,还会暴露你的代码风格和工程习惯。我和一些参与过复试的老师聊过,他们普遍会留意这些细节:变量命名是否有语义、注释是否必要、缩进是否统一、有没有把重复代码抽成函数。这些习惯大概率不是决定项,但一定是加分项。我在平时训练时,会强迫自己按“可读代码”标准写,而不是按“能过样例就行”的标准。具体做法是:变量名用有意义的英文单词,循环变量统一用i、j、k;关键步骤前加一句简短注释,说清楚为什么这样写;超过三行的逻辑块,一定拆成小函数;提交前把调试输出全部删干净。这些习惯到了复试现场,会自然帮助你处理更复杂的题目,也会让老师翻阅你作答记录时多给一点印象分。根据我自身的备考经验,复试编程题考查的从来不是天赋,而是压缩时间内的稳定输出能力。每个能稳定AC五道题的人,背后都是几百道题的刻意训练和反复的错误复盘。你不需要成为竞赛选手,只需要成为一个“在考场上不会因为低级错误丢分”的人。把自己的模板、套路、边界清单不断打磨,这五道题,完全可以稳稳拿下。