如果你准备过大厂的笔试,对《网易2018校园招聘编程题真题集合》这个名字应该不陌生。我前几天整理电脑里的旧文件,又把这份题集翻了出来完整刷了一遍。说实话,这套题放在现在依然有很强的参考价值:它把互联网公司校招笔试里最常碰到的几类问题浓缩在七八道题目里,难度梯度也拉得很开,既有一看就会的送分题,也有需要绕几个弯才能想清楚的综合题。对于正在准备暑期实习、秋招提前批,或者单纯想补算法基础的人来说,这套题都是一份非常合适的练手材料。
下面我会按真题集里的典型题目来拆解:先说清楚这套题到底在考什么,再挑几道最有代表性的题完整复盘解题过程,最后聊一聊刷题时容易踩的坑,以及怎么把一套老题的价值最大化。题目名称和题面我按当年牛客网上的回忆版整理,细节可能有个别出入,但考点和解题思路是可靠的。
1. 这套真题集到底在考什么:先看试卷结构,再谈刷题价值
1.1 试卷结构:七八道题,两个小时的脑力冲刺
网易2018校招的线上笔试一般安排两个小时左右,编程题大概四到八道。这套真题集合里我印象比较深的有魔法币、相反数、字符串碎片、重排数列、操作序列、小易喜欢的数列、游历魔法王国等。题目数量不算多,但每一道都卡在一个常见考点上,几乎没有凑数题。
我当时第一次做这套题的时候,感觉最明显的是时间节奏:前两道送分题大概各需要五分钟,中间两道中档题需要十五到二十分钟,后面两道难题可能要卡上半小时。如果你在送分题上磨太久,后面综合题基本没有思考空间。所以这套题其实也在训练一个很重要的能力——快速识别题目类型,然后决定投入多少时间。
下面这张表是我根据回忆整理的题目与考点对应关系,方便你建立整体印象。
| 题目(回忆版) | 核心考点 | 一句话难点 |
|---|---|---|
| 魔法币 | 数学构造 / 倒推 | 正向模拟状态爆炸,倒推每次减半 |
| 相反数 | 字符串处理 | 反转后前导零要正确处理 |
| 字符串碎片 | 字符串计数 | 连续相同字符块数统计别想复杂 |
| 重排数列 | 数论 / 构造 | 4的倍数、2的倍数与其他数的数量关系 |
| 操作序列 | 双端队列思想 | 找规律后只需按奇偶输出 |
| 小易喜欢的数列 | 动态规划 | 直接枚举转移太慢,需要容斥优化 |
| 游历魔法王国 | 树 / 贪心 | 最优路径与多出来的步数折算 |
1.2 从考点分布看网易的用人筛选逻辑
这套题里的“送分题”并不只是让你热身的,它们承担了筛选功能。比如相反数和字符串碎片,如果你在十分钟内不能AC,说明字符串处理的基本功还不扎实。对于工程类岗位来说,字符串处理是日常开发里最绕不开的东西,笔试考它并不是为了刁难人,而是想快速筛掉基本功薄弱的人。
中档题比如重排数列和操作序列,考的是把数学规律和数据结构状态理清楚的能力。这类题通常不要求你掌握冷门算法,但需要你在有限时间内观察出规律,并且用代码准确地表达出来。很多同学能想出思路,但边界条件写错,最终只能过部分用例,这恰恰是笔试最有区分度的地方。
高难题比如小易喜欢的数列和游历魔法王国,则用来区分哪些人能从“会写代码”进阶到“会分析复杂度、会设计状态”。网易这种大厂筛选的不只是会背题的人,而是能在限定时间内快速抽象问题模型、找到正确算法的人。这也是为什么即使过了这么多年,这套题依然有参考价值:大厂笔试的筛选逻辑没有变。
1.3 为什么一份2018年的题库现在还能用
算法题和业务代码不一样,它没有版本迭代。树的遍历、动态规划、贪心这些核心思想不会过期,你现在去牛客刷题,看到的题目考点仍然大量落在这几个范围里。你可能会说现在面试更卷了,很多公司开始考困难题,但正因为如此,基础题的参考价值更大了:如果连2018年这套题里的中档题都写不利索,直接去啃LeetCode困难题大概率是空中楼阁。
更关键的是,这套题里的很多题目思路在后续笔试里反复出现。比如“魔法币”这种通过最终状态反推操作序列的题,我后来在其他几家公司的笔试题里都见过变体。把老题真正吃透,比盲目刷新题有用得多。
2. 四道必刷题的题面复盘:把“回忆版”题目讲清楚
2.1 魔法币:一个看起来很绕的数学构造题
题目大意:小易一开始有0枚魔法币,他面前有两种魔法机器。第一种机器投入x枚魔法币,可以产出2x+1枚;第二种机器投入x枚魔法币,可以产出2x+2枚。小易每次操作会把当前所有的魔法币全部投入其中一台机器,现在他想正好得到n枚魔法币,请你输出机器操作序列。
举个例子,如果n等于10,可能的输出是类似“122”这样的字符串,每一位表示选择第1台还是第2台机器。
这道题表面上是个模拟题,但如果你从0开始正向搜索,每次有两种选择,状态会呈指数增长。第一次做的时候,我甚至想过用BFS,后来发现n稍微大一点就爆了。正确做法是反过来思考,这也是这道题最有价值的地方:从目标n倒推,根据奇偶性判断最后一步用了哪台机器,因为第一种机器永远产生奇数枚,第二种机器永远产生偶数枚。
2.2 相反数:字符串题里的“送分题”考察点
题目大意:输入一个整数n,把它的十进制表示数字顺序颠倒,去掉前导零后得到一个新的整数rev,然后输出n+rev。比如输入1325,反转得到5231,相加得到6556。
这道题本身不难,但很能考察基础功。有些人会尝试用取模和除法一位一位拆,也能做出来,但代码容易写长,而且容易在处理0的时候出错。更直接的方式是把整数转成字符串,反转字符串,再转回整数。
为什么这道题值得复盘?因为它的核心不是“会不会反转字符串”,而是能不能想清楚前导零的语义。反转后如果开头是0,比如输入1200,反转得到“0021”,转成整数后应该是21,而不是0021。这类细节在笔试里很容易被忽略,一旦忽略就会出现只过部分用例的情况。
2.3 字符串碎片:输出格式比算法更坑的题
题目大意:一个由小写字母组成的字符串,可以看成若干由相同字母连续组成的“最大碎片”。例如“aaabbaaac”可以被切分成“aaa”“bb”“aaa”“c”四块。现在输入一个字符串,要求输出所有碎片的平均长度,保留两位小数。
这道题最核心的洞察是:不需要真的去切割字符串,只需要数一数有多少个“连续相同字符段”。从第二个字符开始,每次遇到当前字符和前一个字符不一样,就把碎片数量加一,最后用字符串总长度除以碎片数量。
我第一次做的时候想复杂了,去构造了一个vector存每一段的长度,再把它们平均。后来发现完全没必要,一个计数器就够。这也是这套真题集的一个特点:很多题看起来像“模拟题”,但最优解往往只是一个小观察。
2.4 游历魔法王国:一道题串起树、贪心和动态规划
题目大意:魔法帝国有n个城市,编号从0到n-1,城市之间用n-1条无向边连接,构成一棵树。小易从0号城市出发,最多走L步,每一步走一条边。他每到达一个城市就算访问过,同一个城市重复经过只算一次,问最多能访问多少个不同的城市。
这道题是整套题里比较有区分度的一道。很多人看到树就紧张,实际上核心在于一个很直观的贪心:如果步数不是很多,那就沿着某一条路径一直走到底,访问到的城市最多;如果步数非常多,那就在某条最长路径走完之后,用剩下的步数去“逛”其他分支。
“最长路径”不是随便选一条,而是从0号城市出发能走到的最深深度。这里我先提示一下,正解不是求整棵树的直径,而是求从0号根节点出发的最深路径,这个细节很容易搞错,后面我会专门推导。
3. 最优解推导全过程:从暴力想法到能AC的代码
3.1 魔法币:自顶向下倒推,每次缩小一半
魔法币的暴力做法是从0开始BFS,每一步尝试机器1或机器2,直到某个状态等于n。问题在于状态是指数级增长的,n一旦超过10^9,搜索空间完全不可接受。
反过来看就简单了。假设当前硬币数是cur,如果cur是奇数,说明它一定是通过机器1从某个x变来的,并且满足cur = 2x + 1,所以x = (cur - 1) / 2。如果cur是偶数,说明它是通过机器2变来的,x = (cur - 2) / 2。每次倒推一步,cur都会减小到原来的一半左右,所以时间复杂度是O(log n)。
string solve(long long n) { if (n == 0) return ""; if (n & 1) return solve((n - 1) / 2) + "1"; return solve((n - 2) / 2) + "2"; }这里有两个要注意的点。第一,n必须用long long,因为题目有可能给到很大的数,int会溢出。第二,递归拼接字符串时,要保证顺序是从第一次操作到最后一次操作,所以倒推得到的机器编号要放在已有字符串的后面,或者先递归再拼字符。
3.2 相反数:string反转与stoi的兼容性
相反数最稳妥的写法就是字符串反转。C++的stoi函数在转换时会自动跳过前导零,所以不需要手动处理“0021”这类情况。
int n; cin >> n; string s = to_string(n); reverse(s.begin(), s.end()); int rev = stoi(s); cout << n + rev << endl;如果你不想用stoi,也可以自己写一个循环:从字符串开头开始,res = res * 10 + (s[i] - '0')。使用这个写法,前导零在数学上会被自然省略,因为“0 * 10 + 0”还是0。关键点是不要把反转后的字符串直接当成数字输出,否则会保留前导零导致格式错误。
3.3 字符串碎片:计数器只需要一个变量
字符串碎片的做法是统计“相邻字符发生变化的次数加一”。从第二个字符开始遍历,如果当前字符不等于前一个字符,碎片数量加一。总长度除以碎片数量就是平均长度,输出时保留两位小数。
string s; cin >> s; int cnt = 1; for (int i = 1; i < (int)s.size(); i++) { if (s[i] != s[i - 1]) cnt++; } printf("%.2f\n", (double)s.size() / cnt);这里需要注意空字符串的情况,但题目一般会保证字符串长度至少为1。另外,如果使用cout输出浮点数,默认精度可能不会保留两位小数,所以这类题建议直接用printf格式化输出。
3.4 游历魔法王国:从根出发的最深路径加剩余步数折算
游历魔法王国的正解分为两步。
第一步,求出从0号节点出发到最远叶子节点的边的数量,记为maxDepth。这可以用一次DFS实现:从0号节点开始,遍历所有子节点,深度取最大值加一。
第二步,根据步数L进行分类讨论。
如果L <= maxDepth,说明你最多只能沿着某一条路一直走,访问的城市数量是L + 1。因为从0出发走一步到达一个城市,走L步最多经过L条边,经过的节点数是L + 1。
如果L > maxDepth,说明你不仅能把从0到最深叶子的路径走完,还会有剩余步数。先沿着最深路径走完,已经访问了maxDepth + 1个城市。剩下的步数是L - maxDepth,由于访问其他分支时,需要从主干上的某个节点进入分支,再原路返回主干,每一次来回需要2步,只能多访问1个新的城市。所以额外可以访问的城市数是(L - maxDepth) / 2。
最终答案取min(n, maxDepth + 1 + (L - maxDepth) / 2),因为城市总数只有n个,不可能超过这个上限。
def dfs(u, parent): depth = 0 for v in g[u]: if v != parent: depth = max(depth, dfs(v, u) + 1) return depth max_depth = dfs(0, -1) if L <= max_depth: print(L + 1) else: print(min(n, max_depth + 1 + (L - max_depth) // 2))这段代码适合用来理解思路,但实际笔试如果n非常大,递归DFS可能会爆栈。建议改成显式栈或BFS来求最大深度。另外,为什么不能直接用全树直径?因为起点固定是0,你第一步只能从0出发选择一条边,如果最长直径的另一端不在0这个方向,你是没法直接沿着整条直径走的。所以必须求从0出发的最深路径。
4. 现场笔试最容易翻车的五个细节:都是血泪经验
4.1 输入输出模式:ACM模式下别用cout刷屏
很多同学平时用LeetCode的函数式编程很顺手,到了牛客这类ACM模式笔试,连最基本的while(cin >> n)都不习惯。这套真题集合里的题目大多是单组输入,但有些题会有多组数据,或者需要你读完整行再处理。
我踩过最蠢的坑是:有一段调试用的cout输出忘了删,结果本地没问题,在线判题一直WA。后来才发现是多余输出干扰了评测比对。所以在提交前,一定要仔细检查有没有残留的调试输出。
4.2 数据范围:n给到10^18时int会哭
魔法币这道题,n有可能给到很大的值。如果函数参数写成int,递归计算(n - 1) / 2时也可能不会立即溢出,但一旦n超过int上限,直接就是错误的。相反数这道题,如果n给到10^9,反转后也可能超过int范围,虽然很多评测不会卡这个点,但用long long更稳妥。
建议养成一个习惯:看到题目里没有明确说明数据范围,或者数据范围描述里出现“10^9”“10^18”这类字样,一律用long long。不需要每次都用大数模板,但至少不要让类型成为扣分点。
4.3 递归深度:不是所有倒推都像魔法币这么浅
魔法币的递归深度是O(log n),非常浅,完全不用担心爆栈。但游历魔法王国这种树上的DFS,如果n是十万级,递归深度可能达到十万层。C++默认栈空间通常只有几MB,递归太深会直接栈溢出。
这时候可以把递归改成迭代:用一个栈模拟DFS,或者直接用队列做BFS求最大深度。我在做这道题的时候就因为递归爆栈卡了很久,后来改成BFS才通过。
4.4 多组用例的初始化:cnt忘清零是头号错误
字符串碎片这类题逻辑很简单,但如果题目是多组输入,你需要在每组输入前把计数器重置。很多人第一组AC,第二组WA,就是因为计数器是在循环外面初始化的,第二组数据累加到了第一组的结果上。
解决方法是把变量声明尽量放在循环体内部,或者每次循环开始前显式初始化。这个习惯不只在网易笔试里重要,在所有ACM模式笔试里都重要。
4.5 浮点输出:保留两位是硬性要求
字符串碎片要求输出平均长度并保留两位小数。用printf("%.2f")最直接。如果你用cout,默认的精度是6位有效数字,可能输出2.25没问题,但如果答案是2.50,cout可能输出2.5,判题系统会认为格式错误。
浮点输出还有一个小坑:除法操作要保证先转成double再除,而不是两个整数相除取整。int / int在C++里结果是整数,直接丢掉小数部分,后面printf再格式化也救不回来。
5. 真题刷完之后,怎么把这些题转化成笔试能力
5.1 建立“考点—题目—错因”对照表
刷完这套真题,别急着把网页关掉。我建议拿一张表,左侧写考点,中间写题目,右侧写自己的错因。比如魔法币对应“逆向构造”,错因可能是“没想到倒推”;相反数对应“字符串处理”,错因可能是“前导零没处理干净”;游历魔法王国对应“树上的最优路径”,错因可能是“求成了整棵树的直径”。
这样做的好处是,一个月后回看这张表,你能直接看到自己的知识漏洞地图,而不是面对一堆AC过的代码发呆。准备秋招笔试时,这张表比任何收藏夹都有效。
5.2 用真题做限时模拟,而不是零散刷
这套题特别适合用来做限时模拟。找两个小时的完整时间段,打开题目列表,不看任何解析,像真正笔试一样把每道题写完。做完之后,再统一对答案、看题解。
为什么要限时?因为大厂笔试最大的敌人不是题目难,而是节奏乱。很多人在一道题上死磕半小时,导致后面简单题都没时间写。限时模拟能帮你建立时间分配的感觉,比如遇到卡壳超过15分钟的题,先标记跳过,等简单题写完再回来。
5.3 把老题当作新题的母题
真题的价值不在答案本身,而在它背后的“母题方法”。魔法币是“逆向构造”的母题;相反数是“字符串反转与边界处理”的母题;字符串碎片是“连续段统计”的母题;游历魔法王国是“树上的贪心与步数折算”的母题。
我在后续刷题中遇到很多类似题,都会下意识去想:能不能从最终状态倒推?多出来的步数要不要除以2?这个习惯就是从这套真题里养成的。吃透老题,新题对你来说往往只是换了层壳。
5.4 补充这套题没覆盖到的高频算法
2018这套题覆盖了字符串、数学、构造、贪心、DP、树,但还有一些高频考点没有出现,比如并查集、二分答案、前缀和、单调队列、图的最短路。建议在刷完这套真题后,按下面顺序补充:字符串处理,排序与贪心,二分,DFS/BFS,动态规划,并查集,图论最短路径。这样才算把真题的价值放大到整个秋招。
最后说一个我自己的习惯:每次刷完一套真题,我会把每道题的代码压缩成一个命名包含考点的文件,比如magic_coin_reverse_push.cpp、string_piece_count.cpp。一个月后重新看这些文件名,就能快速回忆起每个考点的核心套路。这个习惯帮我在后续几家公司的笔试里节省了大量复习时间。网易2018这套题是很好的开始,希望你能把它真正吃透。