秋招季又到了刷题的季节。如果你正在准备后端、算法岗的校招笔试,"网易2019秋招笔试编程题合集(一)"这套题应该是绕不开的。不管你是不是把网易作为目标公司,这套题都值得认真做一遍,因为它的出题风格非常典型:题面不长、场景包装很生活化、算法模型不偏门但特别爱考细节边界。可以说,把这套题吃透,相当于提前适应了大多数互联网大厂笔试的节奏。
这份合集里收录的题目难度梯度拉得比较开,前两道属于"签到题"级别,后两道直接上强度。很多同学考完回来吐槽"小易怎么又在搬砖""牛牛怎么又在找工作",但其实把这些包装剥掉以后,核心考的都是非常经典的算法原型:贪心排序、动态规划、区间维护、构造计数。这篇文章我打算按照"先看全局、再拆考点、后讲代码、最后复盘踩坑"的顺序,把这套题彻底拆开揉碎。
1. 网易2019秋招笔试的考场生态:题量、时间与策略分配
先说一个很多初次参加笔试的同学容易忽略的问题:笔试不是让你把四道题全AC的,而是让你在有限时间内拿尽可能多的分。网易这套题的典型配置是4道编程题,考试时间在90到120分钟之间。按我自己的经验,前两道简单题应该在20到25分钟内搞定,第三道中等题留30分钟,最后一道难题如果30分钟内没有明确思路,就应该果断转向部分分策略,而不是死磕。
网易的命题有个很鲜明的特征:场景叙事极其统一,主角永远是"小易"或者"牛牛"。2019秋招这批题里,你会看到小易开店、牛牛找活、小易数数字、牛牛排字典,这些故事都是包装,核心脱胎于《剑指Offer》和LeetCode中的经典题型,但会故意加一层"现实约束"来增加区分度。比如"牛牛找工作"这道题,本质上是一个性价比贪心问题,但它不是直接给你一组数让你排序,而是给你"工作难度"和"报酬"两个维度,再给你若干个"能力值"去匹配——这就多了一层"如何高效地对多组查询给出答案"的考量。
还有一个考场细节很多人都栽过:网易笔试的输入格式喜欢用多行、多组数据混排。第一行是数组长度,第二行是数组元素,第三行又是另一个参数,稍不留神就会把输入读错。我当年考的时候,就因为在"牛牛找工作"那道题里把伙伴数量和工作数量的输入顺序搞反了,白折腾了十几分钟。所以拿到题目第一件事,不是看算法,是先把输入输出格式圈出来,用样例数据手动推一遍,确认自己读对了。
另外,这套题对时间复杂度的容忍度很微妙。前两题用O(n^2)暴力完全能过,但第三题开始,O(n^2)基本就是超时预定,必须优化到O(nlogn)或者O(n)。而第四题如果涉及组合数、字典序构造这类问题,不光要会算法,还要对数据范围敏感——该用long long的地方用int,直接就是一个测试点都过不去。
总体策略建议是:正着做,先易后难,但绝不恋战。每道题先花两三分钟把题意和样例吃透,再估计一下算法复杂度,如果卡了15分钟没有实质进展,立刻跳到下一题。最后留10分钟统一回头处理没做完的题——哪怕只能过样例、只能暴力解小数据,也比白卷强得多。
2. 从真题看网易命题组最钟爱的四类算法模型
把这一套题放在一起对比,你会发现网易出题虽然场景天天换,但算法模型来来回回就是那么几个。这里我把出现频率最高的四类考点展开讲一下,每一类都配上真题里的典型场景,帮你建立"看见包装就能识别原型"的能力。
2.1 性价比贪心与排序
2019秋招里最典型的贪心题就是"牛牛找工作"。题目大意是:牛牛找工作了,每份工作有一个难度值Di和一份报酬Pi,牛牛有一个能力值Ai,只有当能力值大于等于工作难度时才能胜任这份工作。牛牛有若干个朋友,每个朋友也有各自的能力值,问每个朋友能拿到的最高报酬是多少。
这个场景剥掉之后,就是一个非常经典的"多维匹配最值"问题。核心思路是:按难度从小到大排序所有工作,然后顺序扫描,维护到当前难度为止的最高报酬。为什么这么做是对的?因为能力值越大,可选的工作集合只会扩张不会收缩,所以"当前能选的最高报酬"是单调不减的。每个朋友的能力值只需要在排序后的工作数组里二分找到最后一个难度不超过能力值的位置,然后直接取该位置的前缀最大报酬。
很多第一次做这道题的人会犯一个错误:对每个朋友单独遍历一遍所有工作来找最大值,复杂度是O(n*m),n和m稍微一大就超时。其实只要想到"先排序再预处理前缀最大值",复杂度立刻降到O((n+m)log n)。这就是典型的"空间换时间"思路,也是网易这类快消型笔试题最爱的考法——不考你知不知道贪心,考你能不能把多组查询的重复计算消掉。
类似的原型还有"会议安排最多场次""区间选点最少个数",都是先排序再贪心的套路。如果你在考场上识别出"我需要对多组查询反复做同一件事",第一反应就应该是:能不能预处理?能不能排序后二分?
2.2 动态规划的"小易式"包装
网易的DP题特别喜欢加一层游戏化设定。比如"小易喜欢的单词"这类题,表面是在问一个字符串满不满足某种"喜欢"的条件,实际上是一道自动机+DP的状态转移题。再比如"独立的小易",小易要在一条街上走,每次可以走1步或2步,但某些位置有障碍不能踩,问到达终点的方案数——这就是一个非常标准的线性DP:dp[i] = dp[i-1] + dp[i-2],障碍位置直接置0。
DP的难点从来不是状态转移方程的推导,而是你能不能一眼看出这是个DP问题,以及状态的维度怎么定。网易很喜欢用"地图""行走""跳跃"这样的场景来包装DP,原因就是它们天然适合用"位置"或"步数"作为状态。遇到这类题,我的习惯是先画一条数轴或者状态转移表,把每一步的依赖关系写清楚,再看有没有空间优化的余地。
比如经典的"小易喜欢的单词"那道题,实际上是一个字符串匹配变体,状态是"当前匹配到原串第几位"和"当前匹配到模式串第几位",转移就是字符相等时的推进和失配时的回退,本质上是KMP自动机上的DP。如果你没见过这类题,第一次做很可能完全摸不着头脑;但一旦见过,下次再遇到"小易XX"的字符串题,你会条件反射地往自动机DP上想。
2.3 区间问题与数据结构的轻量运用
网易有时候会把前缀和、差分数组、线段树这类数据结构揉进题目里。2019秋招里有一道"小易的糖果"题,本质是区间加法和区间最大值查询。如果你只会暴力遍历,数据一大就卡死;但如果意识到"多次区间操作"可以用差分数组优化,或者"多次区间查询"可以用线段树/树状数组维护,题目难度就瞬间降级了。
说实话,网易的题目对线段树的考察不会特别深,基本停留在"你能写出单点更新+区间查询"的水平,更多时候用前缀和、滑动窗口就能解决。比如有一道题是问"连续子数组的和等于K的最长长度",这就是典型的滑动窗口或前缀和+哈希表优化。命题组在这里真正想考的是你对"区间和"这个概念的理解——能不能想到用前缀和把O(n^2)的区间枚举降成O(n)的两次前缀和之差。这一点想通了,很多看似复杂的区间题都能迎刃而解。
刷高频题的时候不要只看题解,要刻意练习"从题目描述中提取区间关系"的能力。看到"连续""子数组""区间""覆盖"这些词,条件反射就应该想到前缀和、差分、双指针、滑动窗口这四个工具。
2.4 思维构造与数论计数
每年网易都会有一两道"思维题",2019秋招里的"小易的字典"就是代表。这种题最大的特点是:你一眼看过去完全不知道用什么算法,甚至会怀疑是不是题目出错了。它考的是你在面对非常规问题时能不能通过数学推导和构造找到规律。
"小易的字典"的核心是:给定n个'a'和m个'z',要求按字典序排列所有由这些字符组成的字符串,然后输出第k个。看着字符串,实际上是一个组合计数问题。字典序第k大的字符串,每一位是'a'还是'z'取决于"以当前位为'a'时后面还剩多少种排列",而这个数量正好是组合数C(剩余位置数, 剩余'z'数)。所以本质上你要做的是:从高位向低位逐位决定,每决定一位就减去对应的组合数数量,直到k减到零。
这种题在考场上最考验心理素质。我的建议是:先拿小数据量在草稿纸上手算出规律,再想办法用组合数或递推形式把规律形式化。平时刷题时多积累这类"构造+计数"的题目思路,尤其是那种"输出第k大/第k小"的题目,大概率是逐位构造+计数剪枝的套路。
3. 一道满分代码该怎么写:从暴力递归到剪枝AC
很多同学有个误区:笔试答题只要思路对,代码差不多就能过。但网易的评测系统非常严格,边界条件、溢出、内存超限都会让你白丢分。这一节我就用"牛牛找工作"这道题,完整走一遍从"能跑通"到"满分AC"的全过程,顺便展示一下我在考场上的代码习惯。
3.1 暴力版:能做对,但只能对一半
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n, m; cin >> n >> m; vector<pair<int, int>> jobs(n); for (int i = 0; i < n; i++) { cin >> jobs[i].first >> jobs[i].second; } vector<int> abilities(m); for (int i = 0; i < m; i++) { cin >> abilities[i]; } for (int i = 0; i < m; i++) { int best = 0; for (int j = 0; j < n; j++) { if (abilities[i] >= jobs[j].first) { best = max(best, jobs[j].second); } } cout << best << endl; } return 0; }这段代码的思路是直给的:每个朋友都遍历所有工作,找报酬最高的那个。时间复杂度O(n*m),当n和m都在10^4以上时,基本就是超时。但它有一个好处——在数据量小的测试点上绝对正确。考场上如果实在想不出优化方案,先交一版暴力,能拿一部分分,这比空着强得多。
3.2 优化版:排序+前缀最大值+二分
优化的核心动机很简单:每个朋友的查询都在重复做"在所有工作中找难度不超过能力值的最大报酬",而工作列表是固定的,那为什么不提前处理好呢?
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n, m; cin >> n >> m; vector<pair<int, int>> jobs(n); for (int i = 0; i < n; i++) { cin >> jobs[i].first >> jobs[i].second; } vector<int> abilities(m); for (int i = 0; i < m; i++) { cin >> abilities[i]; } // 按工作难度从小到大排序 sort(jobs.begin(), jobs.end()); // 预处理前缀最大报酬 vector<int> maxPay(n); maxPay[0] = jobs[0].second; for (int i = 1; i < n; i++) { maxPay[i] = max(maxPay[i-1], jobs[i].second); } for (int i = 0; i < m; i++) { // 二分查找最后一个难度不超过能力值的工作 int l = 0, r = n - 1, pos = -1; while (l <= r) { int mid = (l + r) / 2; if (jobs[mid].first <= abilities[i]) { pos = mid; l = mid + 1; } else { r = mid - 1; } } if (pos == -1) { cout << 0 << endl; } else { cout << maxPay[pos] << endl; } } return 0; }这里有几个关键细节值得强调:
为什么排序后还要维护前缀最大值?因为"难度低"不等于"报酬高"。有可能难度为3的工作报酬是100,难度为5的工作报酬只有80。如果只按难度排序然后直接取"难度不超过能力值的最后一个工作"的报酬,就错了。前缀最大值的意义在于:它记录了到当前难度为止,所有可选工作中的最高报酬。这样无论能力值落在哪个区间,前缀最大值都能给出正确结果。
为什么二分查找的返回值是"最后一个难度不超过能力值的位置"?因为我们要找的是"可选集合的右边界",超过这个边界的工作难度太高做不了,而这个边界之前的所有工作都可能被选择。用二分的标准模板,注意等于号应该归入左边(即让pos尽量靠右),这样能保证取到所有可选工作里的最大值。
优化后的时间复杂度是O(nlogn + mlog n),空间复杂度O(n)。在笔试环境下,这个复杂度可以轻松应对10^5级别的数据量。
3.3 再进一步:如果报酬也要按难度压缩怎么办
有些变体题会把工作难度设计成不连续的大范围值,比如难度范围到10^9,但工作数量只有10^5。这时候排序+二分的思路仍然成立,但要注意离散化:把所有出现过的难度值排序去重,然后对每个能力值用lower_bound找到第一个大于等于它的难度位置,再减一得到可选位置。这个技巧在"牛牛找工作"的升级版里很常见,提前掌握能省不少考场时间。
离散化版本我通常这样写:
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n, m; cin >> n >> m; vector<pair<int, int>> jobs(n); vector<int> diff; for (int i = 0; i < n; i++) { cin >> jobs[i].first >> jobs[i].second; diff.push_back(jobs[i].first); } vector<int> abilities(m); for (int i = 0; i < m; i++) { cin >> abilities[i]; } sort(jobs.begin(), jobs.end()); sort(diff.begin(), diff.end()); diff.erase(unique(diff.begin(), diff.end()), diff.end()); vector<int> maxPay(diff.size()); int idx = 0; for (int i = 0; i < n; i++) { while (diff[idx] < jobs[i].first) idx++; maxPay[idx] = max(maxPay[idx], jobs[i].second); } for (int i = 1; i < (int)diff.size(); i++) { maxPay[i] = max(maxPay[i], maxPay[i-1]); } for (int i = 0; i < m; i++) { int pos = upper_bound(diff.begin(), diff.end(), abilities[i]) - diff.begin() - 1; if (pos < 0) { cout << 0 << endl; } else { cout << maxPay[pos] << endl; } } return 0; }这里用upper_bound找到第一个大于能力值的难度,位置减一就是最后一个不超过能力值的难度。代码看起来复杂了一点,但思路和上面的排序+二分完全一致,只是多加了一层离散化来压缩难度范围。
4. 那些年在网易笔试里"白给"的边界条件
刷题刷多了你会发现,很多时候算法想对了,代码也没写错,但提交就是不过。网易的评测系统尤其爱在边界条件上做文章。这一节我把这套题里最值得注意的边界情况集中说一下,全是我自己踩过的坑和帮别人调试时见过的真实案例。
4.1 输入读取的顺序和格式
网易的题输入格式经常是"多行混排",可能是先给工作数量n和伙伴数量m,也可能是先给伙伴数量再给工作数量,每一行的数据个数也不一样。我的建议是:在写代码之前,先用输入样例手动走一遍,确认每个变量对应的是哪一行。尤其注意vector的resize和push_back不要混用——如果你先resize了再push_back,数据会多出来一倍。
另外,有些题的输入数据量非常大,用cin不关同步会导致超时。建议在main函数开头加上这两行:
ios::sync_with_stdio(false); cin.tie(0);这不算作弊,只是让C++的标准输入输出更快一些,笔试环境完全允许。
4.2 能力值小于所有工作难度的情况
在"牛牛找工作"里,如果某个朋友的能力值是1,但所有工作的难度都大于1,那这个人没有任何可选工作,报酬应该是0。很多人在二分查找时没有处理pos=-1的情况,直接访问maxPay[pos],数组越界导致运行错误。注意上面的代码对这种情况做了特判。
同样还有能力值大于所有工作难度的情况,此时pos= n-1,maxPay[n-1]就是全局最大报酬,不用特殊处理,但你要确认自己的二分逻辑在"全部可选"时不会死循环。
4.3 多个工作难度相同但报酬不同
排序之后,相同难度的工作会堆在一起。如果直接对每个位置计算前缀最大值,同一个难度档位的多个工作报酬会被依次更新,最后保留的是该难度下的最高报酬。这恰好是我们想要的。但如果某个难度档位的工作数量很多,而你没有按难度聚合,前缀最大值依然正确,只是可能会有冗余计算——不影响正确性,但会影响性能。
我在实际写代码时会额外注意:排序时pair默认先按first再按second排序,所以相同难度的工作会按报酬从小到大排。前缀最大值递推式max(maxPay[i-1], jobs[i].second)天然会保留该难度的最大报酬,所以不用额外处理。
4.4 数据范围与溢出
这是最隐蔽也最致命的坑。网易2019秋招的题目数据范围通常给到10^9级别,比如报酬可以是1000000000,朋友数量可以是100000。如果你用int存前缀最大值,两个最大值相加就可能溢出,导致答案变成负数。这类问题几乎每年都有人栽,因为样例数据往往很小,本地测试全过,一提交就WA。
我的习惯是:只要题目数据范围里出现了10^9,所有涉及累加、乘法、组合数的变量一律用long long。尤其是那些考组合数的题,C(n, m)在n=30时就已经超过int范围了,如果你用int存组合数表,算到一半就爆了。
组合数问题的标准姿势是用long long加二维数组预计算,比如:
long long C[1005][1005]; for (int i = 0; i <= 1000; i++) { C[i][0] = C[i][i] = 1; for (int j = 1; j < i; j++) { C[i][j] = C[i-1][j-1] + C[i-1][j]; } }如果题目数据范围更大,比如n到5000甚至更高,二维数组就存不下了,这时候需要用乘法逆元预处理阶乘来算组合数。网易2019秋招里那道字典构造题,数据范围控制在100以内,二维组合数表完全够用,但如果你不知道这个预处理技巧,在考场上现场手推组合数公式,很容易越算越乱。
4.5 字典序构造题的"k超过总排列数"场景
"小易的字典"这类题有一个极易漏掉的点:如果k比所有可能排列的总数还要大,答案应该是-1。很多同学在逐位构造时只关心当前位的组合数够不够减,却忘了先判断整体可不可能。你在代码开头要先算出总排列数C(n+m, n),如果k大于这个数,直接输出-1。这个判断不仅是一行代码的事,它决定了你后续的构造过程会不会出现"减到最后k还是正数"的死循环。
还有一点,构造时每选择一位'a',就要从当前组合数中减去"以'a'开头的排列数",这里的"排列数"是指当前剩余位置中,剩余'z'个数的组合数。写着写着很容易把n和m搞混,我的做法是在草稿纸上先写清楚"剩余n个a、m个z"的状态,然后每一步更新n或m,再算组合数。这种题考场上心态越稳,越不容易出错。
5. 考场实战的时间分配与"部分分"求生指南
笔试和平时刷题最大的区别是:考场上有时间压力,而且你不知道评测数据到底有多强。我见过太多平时LeetCode能轻松做出Hard的人,笔试却翻车,原因不是不会做,而是不会"考试"。这一节分享一下我自己总结的考场实战策略。
5.1 拿到题先做三件事
第一,读题+看样例,确认输入输出格式。程序员最怕的不是算法不会,而是数据读错。如果样例能跑通但提交全错,大概率是格式问题。
第二,估算数据范围,确定目标复杂度。如果n在10^3以下,O(n^2)随便写;n在10^5级别,基本必须O(nlogn)或O(n);n在10^6级别,连O(nlogn)都要小心常数,可能需要O(n)的线性算法。这一步想清楚,后面写代码就不会盲目优化或者过度设计。
第三,确定每道题的优先级。我会用1分钟快速扫完四道题的题干,把"一眼知道怎么做"的题标记为高优先级,"有点思路但需要细想"的题标记为中优先级,"完全没有头绪"的题标记为低优先级。然后按照高→中→低的顺序做。
5.2 遇到卡壳题:暴力分也是分
网易的评测系统不会因为你的代码时间复杂度高就判零分,很多测试点允许O(n^2)甚至更高的复杂度。所以当你一时想不出最优解时,先写一个能过小数据、能过样例的暴力版本交上去,至少能拿到一部分分数。然后再在剩余时间里慢慢优化。
这里有一个经验之谈:暴力版拿了分之后,不要急着删掉重写。先把暴力版放在一边,在纸上推导一下"重复计算到底发生在哪里",找到瓶颈后再针对性地优化。比如"牛牛找工作"的暴力版慢在每次查询都重新遍历工作,那么优化方向就是"预先把答案算好,查询时直接取结果"。这个思路可以在10分钟内把一道暴力题改造成满分题。
5.3 一道题最多花多少时间
我的原则是:简单题不超过20分钟,中等题不超过40分钟,难题如果30分钟没有明确思路,果断放弃。这个时间预算不是绝对的,但至少能保证你不会在一道题上耗尽所有时间然后发现还有三道题没做。
有个真实的教训:我认识的一个同学在笔试时跟"小易的字典"这道题死磕了一个小时,最后虽然做出来了,但前面两道简单题只写了暴力版本,有些测试点没过,总成绩反而不如那些"放弃难题、稳拿简单题"的人。笔试不是竞赛,拿满该拿的分,比解出最难的题更重要。
5.4 提交前最后的"三查"
代码写完、样例通过,别急着提交。花两分钟做最后的自查:
- 查输入输出格式:变量顺序对不对?换行符有没有?是否有多余的输出?
- 查边界条件:n=1时会不会越界?所有元素都相同时会不会死循环?最大数据量会不会超时?
- 查数据类型:有没有用int存long long的隐患?数组开得够不够大?
这三查看起来简单,但每一次都能拦住好几个本不该丢的测试点。尤其是"数组开得够不够大"这一点,如果你开了一个长度为n的数组却访问了n+1的位置,评测系统会直接判运行时错误,而本地运行因为内存布局的原因可能根本不会崩。血的教训。
写在最后:这套题到底值不值得反复刷
经常有同学问我:"2019年的题,现在刷还有意义吗?"我的回答是:算法题的底层模型并不会因为年份变化而过时。贪心、DP、二分、前缀和、组合计数,这些永远是大厂笔试的核心考点。网易2019秋招这套题的价值不在于题目本身有多难,而在于它的出题风格和绝大多数互联网大厂高度一致——用生活化的场景包装经典算法,用数据范围考察复杂度意识,用边界条件筛选代码细节。
我自己每年秋招前都会把这套题重新做一遍,每次做都会发现新的问题:有时候是代码风格不够稳健,有时候是对某个算法的理解又深了一层。刷题的意义不只是为了应付笔试,更是在帮自己建立一套稳定的解题思维框架。
如果你正在准备秋招,我建议你把这套题放进你的必刷清单。不要只做一遍就丢,第一次按考场模式限时做,第二次专门分析每道题的算法原型,第三次只看题面在脑海里快速过思路。等你能够做到"看见小易搬砖就知道考什么"的时候,这套题的价值就真正被你榨干了。