又到了每年的模考季,翻出自己2019年参加牛客第五次模考编程题集合的答题记录,感慨还挺多的。当时那套题我做得很狼狈,前两道顺利通过,第三道开始卡壳,最后一道干脆没时间写。现在五年过去,作为已经带过不少校招新人的工程师回头看,这套题的价值其实远不止于"刷一遍对答案"——它几乎是笔试环境里所有隐藏考点的集中展示。
我经常跟准备面试的朋友说,牛客的模考题特别值得反复研究,不是因为题目有多难,而是因为它模拟的就是真实笔试环境:限时、限空间、无法调试太久、一道题卡住就会影响后面。2019年这套五模的题型编排尤其典型,既覆盖了字符串处理、模拟题、动态规划这些笔试常客,又在数据规模和边界条件上埋了不少坑。今天这篇就把我当时复盘这套题的心得整理出来,主要聊聊题型结构、解题套路、踩坑记录和错题复盘的方法,希望给正在刷题准备笔试的朋友一些实际参考。
1. 五年后再复盘:这套题的出题逻辑和求职现实
1.1 为什么"五模"值得单独拿出来说
牛客的模考系列是分轮次推出的,第一套到第五套各有侧重。前面几套通常偏基础,考察的是"你会不会写代码";到了第五套,考察重心已经变成"你在限时和压力下能不能稳定写出正确的代码"。这也是为什么很多人第一次做五模的时候分数很难看——题目本身的难度曲线在第五套有一个明显爬升,而且混合了多个知识点的综合题明显增多。
当时我所在的求职群里,大家在同样时间做完这套题后的分数分布非常有意思:能拿满分的没几个,但集中在及格线上下的人特别多。这个现象本身就说明了问题——出题人在设计这套题时,刻意把"区分度"做在了中档题上。基础题大家都会,压轴题没几个人做得出来,真正决定排名和面试机会的,恰恰是中间那几道"看起来不难,但总有一两个细节会出错"的题目。
1.2 这套题折射的笔试筛选逻辑
站在出题人的角度想,笔试环节的核心目的不是考倒所有人,而是用最短的时间筛掉"代码功底不扎实"和"工程习惯不好"的候选人。2019年五模这套题里,输入输出格式的坑、边界条件的隐晦设置、时间复杂度的隐性要求,其实都在模拟真实业务的排查场景。你写出的代码不仅要能跑,还要在边界情况下不崩,在数据量大的时候不超时。
我当时把四道题全部复盘完,最大的感受是:会做题 ≠ 会考试。这套题里大约有三道题在思路上我都能很快想到正确解法,但在考场上因为一个边界条件没处理好、因为一次输入读取方式不对,硬生生浪费了二三十分钟。这些东西如果不通过模考暴露出来,到真正的笔试现场就是灾难。所以别把模考当成普通的刷题练习,它更重要的是帮你暴露问题。
2. 从题型分布反推备考重心:哪些分必须拿,哪些分可以放
2.1 高频考点与失分权重
复盘2019年这套五模,整体的考点分布大概是这样的(结合牛客历年模考的常见题型结构,这类题的出题范围都比较稳定):
| 题型类别 | 大致占比 | 常见考点 | 失分原因 |
|---|---|---|---|
| 字符串处理 | 25%左右 | 反转、去重、子串匹配、格式化 | 边界不清、分隔符处理错误 |
| 模拟题 | 25%左右 | 按规则一步步执行、日历/游戏规则 | 状态不完整、循环条件错误 |
| 排序与查找 | 20%左右 | 自定义排序、二分查找变体 | 比较器逻辑写反、索引偏移 |
| 数据结构应用 | 15%左右 | 栈、队列、链表、哈希表 | 数据结构选型不当、复杂度超标 |
| 动态规划 | 15%左右 | 背包、最长子序列、状态机DP | 状态定义不清、转移方程漏情况 |
这个分布其实和大多数互联网公司笔试的出题比例相当接近。字符串处理和模拟题加起来占了将近一半,这意味着你不需要成为算法竞赛选手,只要把这两类基础题的套路练熟,分数线就已经稳了一半。反过来,如果你一上来就死磕动态规划和图论,基础题反而翻车,那才是真正的得不偿失。
2.2 基础题才是分水岭
很多人在备考的时候有一个误区:总盯着最难的那道题复习,觉得"只要我能写出压轴题,前面的题目都不在话下"。但真实情况是,笔试里最可惜的丢分不是压轴题没做出来,而是前面的基础题因为各种莫名其妙的原因出错——超时、越界、读入错误、输出格式不对。
我在复盘这套题的时候,把四道题按"必须拿分"和"尽力拿分"做了分类。字符串和模拟相关的题目属于前者,这些题只要思路清晰,代码量不会太大,是笔试的基本盘。后面涉及动态规划或复杂数据结构的题目属于后者,能做出来是加分项,做不出来也不该影响心态。把这个逻辑想清楚,你在考场上的时间分配就会理性很多,不会因为一道难题卡住而放弃整个大局。
2.3 难题的取舍策略
再具体一点说取舍。第五套模考的最后一道题,我当时看了十分钟没有完整思路,果断先跳过去把前面几道题的边界测试全部补了一遍,回头再来看压轴题,虽然最后只拿到了部分分数,但前面三道题都稳住了。这个策略在真实的笔试里非常实用:大部分平台的判题规则是按通过用例比例给分,部分通过也是分,比空着交卷强太多了。
3. 字符串与模拟题的通解套路:把"程序题"变成"填空题"
3.1 输入输出基本功:Python最容易被扣分的"隐形题"
先说一个最基础但最容易被扣分的环节——输入输出。2019年那会儿很多人还在用C++写笔试,但最近几年用Python的人越来越多了,我自己也主要用Python。Python在写算法题时确实快,但输入输出处理上有不少坑。
笔试平台通常支持两种读取方式:input()和sys.stdin.readline()。很多初学者习惯用前者,但模考数据量一大,input()的性能问题就暴露出来了。举个例子:
import sys def solve(): data = sys.stdin.read().strip().split() # 处理所有输入一次性把全部输入读进来再分割,通常比一行行readline()更快,代码也更简洁。但这要求你先把数据结构想清楚:第一行是什么、后面几行是什么、什么时候结束。我在模考时踩过的坑是,有的题目输入数据之间用多个空格分隔,有的用逗号,有的行末还有多余空格,直接用split()有的能处理,有的不能,必须根据题意灵活选择。
一个小技巧:在读入阶段就顺手把数据转换成目标类型,不要等到用的时候再转。这样既能提前暴露格式问题,也能减少主逻辑里的类型转换噪音。
3.2 状态机思路处理文本类题目
说完输入输出,再聊一个我在复盘字符串题时觉得特别有用的套路:状态机。
2019年这套模考里有好几道题,本质上都是"按规则逐字符处理",比如简化路径、解析字符串、判断括号合法性之类。这种题如果你在循环里堆一堆if-elif,写着写着就会漏掉状态之间的转换关系。更好的方式是先把状态画出来,想清楚每个状态下遇到什么字符应该跳到哪个状态,然后再写代码。
举一个典型的例子,判断一个字符串是否是合法的数字(包含整数、小数、科学计数法):
def is_number(s: str) -> bool: s = s.strip() if not s: return False i = 0 n = len(s) # 跳过正负号 if s[i] in '+-': i += 1 is_num = False is_point = False is_exp = False while i < n: c = s[i] if c.isdigit(): is_num = True i += 1 elif c == '.' and not is_point and not is_exp: is_point = True i += 1 elif c in 'eE' and is_num and not is_exp: is_exp = True is_num = False # 指数部分必须重新出现数字 i += 1 elif c in '+-' and i > 0 and s[i-1] in 'eE': i += 1 else: return False return is_num这种代码你直接硬写很容易漏掉"e后面必须跟数字"、"小数点只能出现一次"这些细节。但如果你先在草稿纸上把状态列出来——起始态、符号态、整数态、小数态、指数态——再对应写代码,每个分支都有依据,正确率会高很多。
3.3 典型模拟题的答题节奏
模拟题的解题节奏我总结为三步:读清楚规则、确定数据结构、逐步实现并打印中间结果验证。
以一道常见的模拟题为例,要求模拟一个排队叫号系统:给定操作序列,包含"顾客到达"、"窗口叫号"、"顾客离开",输出最终每个窗口服务的顾客数。这种题不涉及高深算法,但你需要把每个操作对应的数据结构想清楚。队列存等待顾客,数组存窗口状态,每个操作是一个事件,按时间顺序处理就行。写完后一定要用题目给的示例数据手动跑一遍,再提交。
很多人在模拟题上丢分不是因为不会,而是因为没有按示例逐步验证。我自己习惯在写完模拟题后,把关键变量的变化过程打印出来,和题目示例的中间过程比对,一次就能定位逻辑错在哪。
4. 动态规划和数据结构题:识别变体的三条经验
4.1 DP题的状态定义是灵魂
动态规划在模考中通常只占一道题,但这一道题往往就是区分度所在。我复盘2019年这套五模里的DP题(当时考的是类似"最长有效括号"的变体)时,最大的体会是:状态定义对了,转移方程就是顺水推舟;状态定义错了,怎么调都调不对。
动态规划题的核心步骤永远只有两步:第一步是定义dp[i]表示什么,第二步是找出dp[i]和前面状态的关系。以"最长有效括号"为例:
def longest_valid_parentheses(s: str) -> int: n = len(s) dp = [0] * n res = 0 for i in range(1, n): if s[i] == ')': if s[i-1] == '(': dp[i] = (dp[i-2] if i >= 2 else 0) + 2 elif i - dp[i-1] - 1 >= 0 and s[i - dp[i-1] - 1] == '(': dp[i] = dp[i-1] + 2 + (dp[i - dp[i-1] - 2] if i - dp[i-1] - 2 >= 0 else 0) res = max(res, dp[i]) return res这个题我第一次做的时候,状态定义成"以i结尾的最长有效括号长度",这个定义本身没问题,但转移方程里dp[i-1]的匹配情况容易漏。后来我总结了一个经验:DP题写完转移方程之后,一定要找一组比较长的测试用例,手动按索引推一遍,确认每个分支都覆盖到了,再提交。
4.2 数据结构选择的快速判断
模考里的数据结构题通常不会要求你手写红黑树这种高级结构,考得最多的还是栈、队列、链表和哈希表。关键是要快速判断出该用哪个结构。
我记得那套题里有一道链表相关的题目(类似"链表中的下一个更大节点"),很多人在考场上第一反应是双重循环,O(n^2) 的复杂度在数据量一大就超时。但实际上用单调栈可以做到 O(n)。这个经验很重要:遇到"找下一个更大/更小元素"这类问题,优先想到单调栈;遇到"最近配对"问题,优先想到栈;遇到"滑动窗口最值",优先想到双端队列。数据结构选对了,很多题就是模板题。
这里可以给大家一个快速判断的小表格:
| 题目特征 | 优先考虑的数据结构 |
|---|---|
| 最近匹配、嵌套关系 | 栈 |
| 排队、公平调度 | 队列 |
| 高频访问、快速查找 | 哈希表 |
| 数据量小但频繁增删 | 链表 |
| 区间最值、滑动窗口 | 双端队列 / 堆 |
4.3 时间复杂度估算:动手前先算一算
2019年那套题里有一道排序变体题,题目数据范围给到了 10^5,意味着 O(n^2) 的解法肯定会超时。我当时第一反应是直接sort加自定义比较器,这个选择是对的,因为 Python 内置排序是 Timsort,平均 O(n log n),完全能过。但如果你在考场上写了冒泡或者插入排序,那大概率会卡在超时上。
做题之前花十秒钟估算一下复杂度,是笔试的基本习惯。10^5 的数据规模,n log n 的算法大约是 10^6 次操作,Python 可以轻松承受;n^2 则是 10^10 次操作,Python 绝对跑不动,C++ 也够呛。所以读完题先看数据范围,再选择解法,比什么都重要。
5. 模考实战踩坑记录:五个高频翻车点与止损方案
5.1 输入读取的"隐形坑"
第一道题我当年就栽在输入读取上。题目给的是多组测试数据,每组之间有空行,我直接用input()一行行读,结果遇到空行就报错。后来改成用sys.stdin.read()把全部内容一次性读进来,再按规则分割,问题才解决。
Python 笔试里,如果题目说"输入包含多组测试数据,以 EOF 结束",不要用input()在循环里判断是否读到空字符串,那样在牛客的判题环境里很容易出问题。正确做法是:
import sys for line in sys.stdin: line = line.strip() if not line: continue nums = list(map(int, line.split())) # 业务逻辑5.2 边界条件的"差一错误"
差分、握手问题、数组循环,这些场景里的边界是最容易出 bug 的。我复盘自己那次的失分点,最典型的错误是循环里用了while i <= n但数组长度只有 n,访问arr[i]时越界。Pyt hon 里列表越界不会像 C++ 那样直接崩,但会抛IndexError,而且这个错误往往是在最后一个用例才触发,前面都正常,排查起来特别费劲。
我的经验是:凡是涉及索引的循环,先问自己三个问题——循环变量从 0 开始还是从 1 开始?终止条件是<还是<=?数组最后一个元素的下标是多少?这三个问题想清楚再写循环,基本能规避掉一半以上的越界 bug。
5.3 时间换空间的错误判断
第五套模考里有一道题,我第一反应是开一个超大数组做记录,这样可以 O(1) 判断某个值是否出现过。但题目给定的值域特别大,开到 10^9 的数组肯定不现实。这就是典型的"空间换时间"的错误用法。正确做法是用哈希表,Python 的dict或set在数据量可控的情况下表现得很好。
所以遇到"需要快速查找"的题目,先评估值域范围。值域在 10^6 以内,可以用数组;值域更大,老老实实用哈希表。这两者的性能差距在笔试数据规模的测试下往往不大,但代码健壮性区别很大——数组开太大直接内存超限,哈希表只是稍微慢一点。
5.4 递归深度问题
Python 的递归深度默认只有 1000 层,这在遇到树形结构或者某些递归写法时会直接爆RecursionError。我在模考时遇到一道需要遍历二叉树变体的题目,递归函数写到一半突然报错,当时整个人都懵了。
解决办法有两种:一是提前设置递归深度上限:
import sys sys.setrecursionlimit(10**6)二是把递归改成显式的栈迭代。在笔试场景下,我强烈建议优先尝试第二种。因为设置递归深度只是缓兵之计,如果递归本身写得不高效,照样会栈溢出或超时。很多树的题用迭代写法反而更直观:
# 二叉树前序遍历的迭代写法 def preorder(root): res = [] stack = [root] while stack: node = stack.pop() if node: res.append(node.val) stack.append(node.right) stack.append(node.left) return res5.5 卡题止损:三十分钟原则
聊一个心态层面的坑。模考和真实笔试一样,最怕的就是在一道题上死磕。我当时在第二道题上耗了太多时间,导致后面整体节奏崩掉。后来我给自己定了一个规矩:一道题超过 30 分钟没有完整 AC 思路,立刻标记为"待定",赶紧做后面的题。等所有会的题都拿完分,再回头啃硬骨头。
这个止损策略在我后续的笔试里帮了大忙。要知道笔试的判题规则是按通过的用例给分,你花 30 分钟把一道题从 40% 通过率提升到 60%,远不如用 15 分钟把另一道题从 0 提升到 100%。时间管理的优先级永远是把容易的分先拿到手。
6. 错题复盘模板:一套题怎么榨出三倍价值
6.1 错题分类的三个维度
拿到模考成绩之后,别急着看题解,先自己给错题分类。我常用的分类维度有三个:
- 知识盲区:这个考点我完全没见过,或者知识体系里没有对应的解法。
- 疏忽失误:思路正确,但边界、输入、类型转换等细节出错。
- 复杂度问题:思路正确,但超时或超内存。
把每道错题归入其中一个维度,复习的思路就清晰了。知识盲区需要补充学习,疏忽失误需要加强练习,复杂度问题需要优化思维。这三个维度对应完全不同的提升策略,混在一起复习效率很低。
6.2 三轮复盘法
一轮复盘(当天):合上题解,把错题重新独立做一遍。如果还是不能 AC,说明思路没有真正掌握,而不是粗心。
二轮复盘(三天后):不看任何笔记,重新做这四道题。这次重点看能不能想起当时的思路,并且用更简洁的方式实现。
三轮复盘(一周后):只做"变式"——把原题的条件改一改。比如把"最长有效括号"改成"删除最少的括号使字符串有效",看自己在新的场景下能不能迁移思路。
这三轮下来,一套模考的价值基本上能顶三套题。很多人的问题是只做了一轮复盘,当时看懂了题解就以为自己会了,结果一周后再做还是不会,就是这个原因。
6.3 建立个人错题档案
最后分享一下我的错题档案模板。我不太建议抄题,那样太费时间,我通常只记录几个关键字段:
- 题目来源与编号(比如"2019牛客五模 T3")
- 涉及考点(2-3 个标签,如"动态规划/栈/字符串")
- 错误原因(从三个维度选一个)
- 一句话解法总结(比如"单调栈维护索引递减,遇到更大元素时弹出并记录答案")
- 再错次数
用 Excel 或笔记软件维护都行,关键是每次笔试前把这本档案翻一遍。你会发现真正反复错的永远是那几个点,把它们攻克了,笔试水平会有一个很明显的提升。
我自己的体会是,模考分数一点点上涨的过程,其实就是错误档案从厚变薄的过程。做这套2019年第五次模考的时候,我的档案里记录了很多现在看来很基础的错误,但正是这些错误帮我在后来的笔试里避开了同样的坑。如果你现在正在准备校招或者跳槽笔试,真心建议不要只刷题不对答案,找一套完整的模考,严格按考场时间做一遍,再按上面这套方法复盘。把每一次模考都当成发现问题的机会,比单纯追求刷题数量要有效得多。