2018年春天,牛客网推出校招笔试一模,我第一时间报了名。当时身边很多人只是把它当成一次普通的在线编程题练习,考完看一眼分数就翻篇了。我后来发现,这份B卷的价值远不止分数——它几乎把校招笔试里最常出现的题型、最容易踩的坑全浓缩在了两小时里。这篇文章不是真题搬运,而是基于B卷常见题型方向做的一次完整复盘:哪几类题必须拿满分,哪些题是用来筛选思维能力的,现场提交时最容易在哪些细节上翻车。如果你正在准备春招、暑期实习,或者刷了不少题但心里没底,这篇复盘应该比单纯再刷十道题更有用。
1. 先看全局:2018年一模B卷到底在考什么
1.1 从A卷到B卷的定位差异
一模通常分A卷和B卷,A卷偏热身,B卷的难度会明显高一截。参加过的人应该都有感受,A卷前两三道基本是送分题,考察的是你“会不会写基础语法”;到了B卷,送分题还在,但后面会混入更多需要动脑子的题目,尤其是字符串边界处理、贪心策略和入门动态规划。
我记得B卷整体题量大概在10到15道之间,考试时长两小时左右。题量看着不多,但如果每道题都磨蹭,时间根本不够用。当时我身边不少同学A卷能提前半小时交卷,到了B卷却卡在第二三题就慌了。原因也很简单:B卷更强调“代码实现能力+边界处理意识”,你不仅得想出解法,还得在限时内写出能扛住OJ各种边界用例的代码。
1.2 高频考点大致分布
虽然不能说每套一模B卷题目完全一样,但根据我和几个一起刷题的同学复盘,考点分布是有明显规律的。下面这张表是我根据常见题目方向整理出来的,不是真题原文,但对备考很有参考价值:
| 考点类型 | 大致题量 | 考察重点 | 难度定位 |
|---|---|---|---|
| 字符串处理 | 3道左右 | 反转、子串、字符统计、单词切分 | 送分到中等 |
| 数组与模拟 | 3道左右 | 排序、指针移动、矩阵遍历 | 送分到中等 |
| 贪心 / 排序 | 1-2道 | 区间、调度、局部最优推导 | 中等偏上 |
| 动态规划 | 1-2道 | 最大子数组、最长上升子序列等基础模型 | 中等偏上 |
| 数学 / 逻辑 | 1道左右 | 进制转换、素数判定、规律找式 | 送分到中等 |
这个分布和春招笔试的主流风格是吻合的。如果你发现自己某个模块特别薄弱,那么一模就是最好的“探照灯”,能把你平时刷题没注意到的盲区照出来。
1.3 难度梯队与时间分配建议
从实战角度,我把B卷的题目分成三个梯队:
第一梯队是签到题,大概占2到3道,主要考察输入输出、循环、数组基本操作。这类题的目标是“又快又对”,最好在10分钟内搞定。
第二梯队是字符串和模拟题,大概3到4道,需要细心处理边界条件。这里容易出现“思路秒杀、代码翻车”的情况,建议每道题控制在20分钟左右,留出时间反复检查。
第三梯队是贪心和动态规划,通常1到2道,难度取决于你是否见过类似的模型。这类题如果5分钟内没有清晰思路,先跳过,最后再回来啃,不要死磕。
我当时定的节奏是:前20分钟做掉签到题和前两道模拟题;中间50分钟处理剩余模拟题和字符串题;最后50分钟集中攻克DP、贪心和那道综合题。这个节奏不一定适合所有人,但“先保底、再突破”的思路是通用的。
2. 字符串与模拟题:送分题里的失分重灾区
2.1 典型题目方向与读题陷阱
字符串处理在B卷里出现频率极高。常见的出题方向有:反转字符串中的单词顺序、统计出现次数最多的字符、判断是否存在回文子串、去除重复字符后按字典序排序。这些题单独拿出来都不难,但在线上OJ环境里失分率反而特别高,原因就一个字:边界。
我印象很深的一道题型是“反转字符串中的单词顺序”。题目会给一句英文,要求把单词顺序倒过来,比如"I am a student"要变成"student a am I"。很多人的第一反应是先把整个字符串反转,再逐个单词反转回来,这个思路是对的,但现场写起来很容易忽略两个细节:一是字符串前后可能有多余空格,二是单词之间可能存在多个连续空格。
2.2 最容易被卡住的三个边界
第一个边界是空串。有些题目不会明确说输入一定非空,如果代码里直接访问下标,空串就会导致越界。第二个边界是连续空格,如果不做处理,切出来的“单词”会混入空字符串,导致结果错误。第三个边界是输出格式,比如最后不能有多余空格,很多OJ对末尾空格是判错的。
处理空格问题,我推荐一个省心的技巧:用C++的istringstream,它天然会忽略连续空格,按空白字符切分单词。Java里可以用split("\\s+"),Python里更简单,直接用split()。但要注意,C++的istringstream在遇到中文或非ASCII字符时行为可能不符合预期,所以做题前先确认题目输入范围,通常校招笔试都是纯英文字母加数字。
2.3 一个标准示例:反转字符串中的单词
以“反转单词顺序”这道经典题为例,我最常用的写法是这样:
#include <bits/stdc++.h> using namespace std; string reverseWords(string s) { string res = ""; istringstream iss(s); string word; vector<string> words; while (iss >> word) { if (!word.empty()) { words.push_back(word); } } for (int i = (int)words.size() - 1; i >= 0; --i) { res += words[i]; if (i > 0) res += " "; } return res; }这里的核心逻辑是:先用istringstream按空白切好单词存入words,再从后往前拼接。每次拼接时判断i > 0才加空格,避免末尾多一个空格。word.empty()的保护虽然在这种场景下几乎不会触发,但写上更稳妥。
如果同样的题用Python写,代码更短:
def reverseWords(s: str) -> str: words = s.split() return " ".join(words[::-1])Python的split()默认就是按任意空白字符切分,并且自动忽略空串,输出时再用空格join回去,整道题两行解决。这也是为什么我后来练手经常用Python,笔试时如果允许选择语言,这类字符串题用Python确实省心。
2.4 现场做题的复盘教训
一模那场我在这类题上丢过分,复盘时发现了三个问题。第一个是读题太快,没注意到题目要求“输出单词间保留单个空格”,我以为原样输出就行。第二个是测试用例造得太少,只测了常规句子,没测前后空格和全空串。第三个是心态问题,写完后没有回头读一遍代码,结果把words[i]写成了words[i-1],这种低级错误在OJ上就是整题零分。
从那以后,我给自己定了个规矩:每道字符串题至少造三组特殊用例——空串、全空格、极限长度串。这个习惯一直保留到现在,确实帮我少丢了很多冤枉分。
3. 贪心和动态规划:B卷拉开差距的两座山
3.1 贪心题的识别标志
一模B卷里,贪心题不像字符串题那么直白,它通常会包装成一个生活化的场景,比如“安排最多的活动”“用最少的硬币凑出某个金额”“切分木板的最小代价”。这类题有一个共同特点:看起来需要尝试很多种组合,但如果你能找到一个局部最优策略,并且这个策略能一步步推出全局最优,那么它就是贪心题。
识别方法其实很直接:如果题目问的是“最多”“最少”“最短”这种极值问题,而且候选决策之间没有复杂的相互依赖关系,可以先往贪心上想。考场上的建议是先举几个小例子验证策略,比如“每次选结束时间最早的活动”是否真的能安排最多活动,如果小例子都能通过,就大胆写,不要过度纠结证明。
我见过不少人在考场上明明想到了贪心策略,却因为怕有反例而不敢写,最后浪费大量时间。其实在笔试时限下,正确的策略是用小样例验证后快速实现,而不是在草稿纸上做完整数学证明。当然,平时训练时还是要补证明能力,但那是另外一回事。
3.2 DP题的读题信号
动态规划在B卷里出现的概率很高,但难度不会一下拉到太难的程度。最常见的两个基础模型是最大子数组和与最长上升子序列。这两类题目有一个非常明显的信号:当前状态可以由之前的状态直接推导,而且题目要求的最优解具有“重叠子问题”的结构。
以最大子数组和为例,题意通常是一组数中找出连续的一段,使它们的和最大。最直观的做法是双层循环枚举所有连续区间,时间复杂度O(n²),但B卷往往会把数据范围调到n=10的5次方甚至更大,O(n²)必死。这时候就需要用DP。
3.3 状态定义与转移手把手
最大子数组和的状态定义非常经典:设dp[i]表示以第i个元素结尾的最大子数组和。转移方程就一行:
dp[i] = max(nums[i], dp[i-1] + nums[i])这个方程的意思是,以当前元素结尾的最大子数组,要么只包含当前元素自己,要么接在前一个元素结尾的最大子数组后面。最后答案就是所有dp[i]里的最大值。如果要讲得更直观:你可以把它想象成在连续爬坡,如果前面的累积收益是负的,那不如从当前重新起步。
代码实现可以进一步优化,因为dp[i]只依赖dp[i-1],完全不需要开数组:
class Solution { public: int maxSubArray(vector<int>& nums) { int cur = 0; int ans = INT_MIN; for (int x : nums) { cur = max(x, cur + x); ans = max(ans, cur); } return ans; } };这个模式我推荐所有准备笔试的同学背下来,因为它不仅是最小代码量的写法,还能避免数组越界问题。我第一次在牛客OJ上提交时,用的是开dp数组的写法,后来发现用滚动变量不仅更省空间,逻辑也更不容易出错。
3.4 先写暴力再写优化,考场上的救命原则
无论DP还是贪心,我在B卷现场体会最深的一条原则是:如果你不能立刻想到最优解,先写一个暴力解法保底。理由很简单——暴力解法虽然可能超时,但至少逻辑正确,能拿到一部分测试点的分;而空着不写,一分都没有。
暴力写完后,再尝试用滚动数组、二分、排序等手段优化。比如最长上升子序列,暴力DP是O(n²),优化版用贪心+二分可以做到O(n log n)。但在考场上,如果不确定优化版能否写对,我宁可先交O(n²)版本,等做完全部题目,再回头看这道题是否能优化。这种“先保底、再升级”的策略,能最大化整张卷子的得分。
4. 提交运行时那些坑:超时、溢出与静默错误
4.1 时间复杂度自测
B卷的题面一般会给出数据范围,这是判断时间复杂度的关键线索。我当时的经验是:1秒的时间限制内,大约能运行1e7到1e8次基本操作。也就是说,如果n是1万,O(n²)勉强能过;如果n是10万甚至100万,O(n²)基本必超时。
| 数据范围n | O(n) | O(n log n) | O(n²) |
|---|---|---|---|
| 1万 | 安全 | 安全 | 勉强 |
| 10万 | 安全 | 安全 | 危险 |
| 100万 | 安全 | 可以 | 必挂 |
做题前先花10秒估算一下复杂度,能避免很多冤枉的超时。比如一个双层循环遍历数组,n是10万,那就是1e10次操作,任何OJ都扛不住。
4.2 整数溢出的坑
B卷里经常出现累加、乘法运算,C++选手尤其要小心int溢出。很多人在本地测试时一切正常,一提交就是WA,原因就是数据范围一大,int直接爆掉了。最典型的场景是求数组和、求阶乘、求组合数。
我的习惯是:只要题面数据的绝对值可能超过10的4次方,或者涉及累加求和,统一用long long。不要觉得写long long显得不专业,在OJ上这叫稳妥。还有一类坑是负数情况下的取模运算,C++里负数取模的结果可能是负的,如果题目要求输出非负,就得先加模数再取模。
4.3 输入输出格式细节
输入输出是B卷另一个失分点。常见的问题包括:多组测试数据要用while (cin >> n)循环读入,不能只读一次;输出要求每个结果占一行,不能用空格分隔;要求精确到小数点后几位时,用printf或iomanip设置格式。
我记得一道模拟题,题目要求输出两个数字之间逗号加空格分隔,我只写了逗号没加空格,结果WA。这种错误在本地根本看不出来,因为人眼会自动忽略格式差异,但OJ不行。所以提交前一定要逐字核对题目输出格式,尤其是在“注意”“提示”这些地方。
4.4 现场调试三板斧
第一板斧是造边界用例,空串、零、最大值、最小值、极端输入,每道题提交前都过一遍。第二板斧是打印中间状态,在代码里临时加cout或printf输出关键变量的值,确认逻辑走到哪一步出了问题,定位后立刻删掉。第三板斧是用assert检查不可能出现的情况,比如数组下标应该在[0, n)之间,如果断言失败,说明前面的逻辑有bug。
这三板斧看着简单,但真到考场上能救命。我B卷最惨的一次是字符串题多了一个换行符,肉眼完全看不出来,最后用printf("[%s]", res.c_str())一打印才发现末尾多了个不可见字符。
5. 模考成绩出来后怎么用:从分数段到刷题路线
5.1 分数段对照表
模考成绩出来后,最忌讳的事情是看一眼总分就关掉页面。正确的做法是先按题型统计得分率,再对照自己的目标岗位判断短板。我整理了一个粗略的分数段参考:
| 得分率 | 状态评估 | 建议行动 |
|---|---|---|
| 30%以下 | 基础和心态都还需要练 | 回到入门题,先把输入输出、数组、字符串基本功打牢 |
| 30%-50% | 能写基础题,但模型题薄弱 | 重点补排序、二分、贪心和简单DP |
| 50%-70% | 常见题型基本掌握,但容易被边界和复杂场景卡住 | 系统做题,每道题都要求自己处理边界情况 |
| 70%以上 | 笔试能力较强,可以冲击难题 | 刷真题和综合题,训练解题速度 |
对照完分数段,再去看具体错了哪些题:是因为题意理解错,还是解法复杂度不够,还是代码写错但思路正确?这三种错误的复习方式完全不同。我当时的复盘方式是建一个Excel表,列了题号、错误类型、错误原因、正确思路、可复用的技巧五列,每套卷子都填。虽然有点麻烦,但一个月下来,自己常犯的错误一目了然。
5.2 一个可执行的四周补强计划
如果你一模B卷做完后觉得自己还没到理想水平,这里给你一个我实际操作过、也推荐给学弟学妹的四周计划。
第一周专攻字符串和模拟,每天刷5道左右字符串题,重点练split、substring、反转、双指针这些操作;第二周专项练排序、二分和双指针,重点理解有序数组和指针移动的套路;第三周开始接触贪心和DP,最大子数组、最长上升子序列、零钱兑换这几个模型必须滚瓜烂熟;第四周做牛客真题模考,每周至少3套,每套都严格按照笔试时间和心态要求自己。
这个计划的核心思路是用一周时间集中突破一个题型,而不是每天泛泛地刷10道不同题。题型集中训练的好处是能快速总结规律,形成条件反射,考试时看到题目就能立刻锁定考点。
5.3 这套题对面试和后续学习的长尾价值
很多人觉得模考卷做完了就算完成任务,其实不是。B卷里的字符串题、DP题、贪心题,在后续面试手撕代码环节出现概率极高。尤其是“反转字符串中的单词”“最大子数组和”这类的题,我在面试里遇到过不止一次。模考时走过的坑,面试时就能少踩一个。
而且这套题型训练对后续各类编程等级考试也有帮助。我今天还看到有人在问python一级编程题的事,其实不管是牛客笔试还是等级考试,基础的数据处理、字符串切分、循环和条件判断都是同一套底层能力。不要因为年份变了、平台变了就觉得要重新学一遍,基本功到位了,任何考法都能快速适应。
最后分享一个我从一模之后一直保留的习惯:每套题考完,必须留10分钟做复盘笔记。不是把代码贴一遍就完事,而是记录“我为什么卡住”“下次怎么做能更快”。这个动作在当下看着不起眼,但坚持几个月后,你会发现同类错误越来越少,解题速度也会有明显提升。就像一模B卷那道让我丢分的字符串题,它教给我的不是某一个函数怎么用,而是“读题后再动手、提交前必查边界”这十二个字。这套卷子真正的价值,也正是从这些教训里长出来的。