十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

牛客一模算法笔试复盘:KMP、Dijkstra与动态规划实战解析

牛客一模算法笔试复盘:KMP、Dijkstra与动态规划实战解析 考过牛客模考一模算法笔试的同学应该都有一种明显的感受这套题不追求“偏难怪”而是非常贴近真实校招笔试的调性——选择题考基础扎实不扎实编程题考你在有限时间里能不能把思路快速转成正确代码。我完整跟过一轮牛客模考一模这套卷子做完之后最大的感想是它能暴露出来的问题比闷头刷十套LeetCode还多尤其是那些你以为会、但一上考场就卡壳的知识点。牛客的模考系统大家都不陌生它模拟的是主流互联网公司笔试环境的整套流程。不同公司用牛客笔试时的题量和难度差别不小但一模这套卷子基本是“标准版”约20道选择题加2到4道编程题时长120分钟。选择题覆盖数据结构、算法、语言基础、操作系统和计算机网络编程题主要集中在字符串处理、图论最短路、动态规划和贪心策略上。这个结构和很多公司校招技术岗的笔试高度重合所以无论你面后端、前端还是算法岗参加一次模考的参考价值都很大。下面是我复盘这套卷子时的完整拆解包括选择题高频考点的推导过程、编程题的思路和可运行代码以及牛客笔试系统本身的注意事项。如果你正准备下一轮校招或者正处于刷题阶段这篇文章应该能帮你少走不少弯路。1. 先把这套卷子看明白模考形式和考点分布1.1 牛客模考的试卷构成与时间分配先说试卷形式。我参加的一模算法笔试前半小时基本都在跟选择题较劲。选择题中数据结构相关的一般占7到9道算法设计类5到6道剩下的分散在C/Java语法、计算机网络和操作系统里。千万别小看这些选择题很多题不是你会不会而是“你能不能在规定时间内快速做出判断”。模考总时长通常是120分钟如果选择题磨蹭太久后面编程题大概率做不完所以我习惯把选择题控制在40分钟以内遇到拿不准的先标记回头再处理。编程题一般是2到4道难度呈梯度上升。第一题往往是简单模拟或字符串处理属于“送分题”但只要有边界条件没考虑清楚就可能卡很久。中间一到两题考察经典算法比如最短路、DP、贪心这部分是区分度最高的。最后一题如果出现通常是综合应用或偏优化的题拿到部分分也比空着强。1.2 一模的核心考点清单把整套卷子做完后我统计了一下考点分布整理成下面这张表供大家参考考点类别具体知识点一模出现频率备注重难点数据结构数组、链表、栈、队列、哈希表高链表反转、哈希冲突处理树与二叉树遍历、二叉树性质、堆高前中后序遍历转换、堆调整字符串KMP、模式匹配中高next数组推导、匹配计数排序算法快排、堆排、归并、冒泡高复杂度、稳定性、手写实现经典算法二分、贪心、DP高二分答案、状态转移设计图论Dijkstra、拓扑排序、并查集中优先队列优化、路径打印数学与杂项快速幂、位运算、STL使用中溢出处理、取模运算有同学会问像粒子群算法、模拟退火、卡尔曼滤波、机器学习、深度学习这些热词里的算法笔试会不会考以我的经验传统算法笔试很少直接让你手写粒子群或者SVM这些更多出现在算法岗的专业方向笔试或者面试问答里。但选择题有可能会给你一段“群体迭代寻优”的描述让你判断这是哪种算法。所以备考时至少要能区分常见算法的基本思想而不是只会背名字。另外不管你是走后端、前端还是算法方向这套卷子里的选择题都值得认真对待。因为这代表的是计算机基础素养很多公司即使招非算法岗也喜欢用这类题快速筛人。2. 选择题高频考点拆解从KMP到排序2.1 KMP算法next数组推导实战先说KMP。热词里有一个非常典型的例子模式串 pabacaba求其next数组。这个问题在一模乃至正式笔试中出现的频率都不低因为它考的不是你能不能背出代码而是有没有真正理解“前缀函数”的意义。我先把模式串的字符和下标列出来然后手动推导一遍下标 i0123456字符abacabanext[i]0010123推导过程是这样的next[0] 0因为长度为1的子串没有真前缀和真后缀。i1字符是b子串ab的公共前后缀长度为0所以next[1]0。i2字符是a子串aba中前缀a和后缀a相同长度为1所以next[2]1。i3字符是c子串abac最长公共前后缀为0所以next[3]0。i4字符是a子串abaca前缀a和后缀a相同所以next[4]1。i5字符是b子串abacab最长公共前后缀是ab长度为2所以next[5]2。i6字符是a子串abacaba最长公共前后缀是aba长度为3所以next[6]3。这个推导过程其实就是KMP求next数组的暴力逻辑笔试中常见考法有两种一种是给出几个next数组选项让你选另一种是让你判断某一步匹配失败后模式串应该怎么移动。代码实现上我习惯写下面这个版本它在很多OJ上都验证过void getNext(const string p, vectorint next) { int n p.size(); next.resize(n); next[0] 0; int j 0; for (int i 1; i n; i) { while (j 0 p[i] ! p[j]) { j next[j - 1]; } if (p[i] p[j]) { j; } next[i] j; } }这里有个特别容易搞混的点不同教材对next数组的定义不一样。有的定义为“最长公共前后缀长度”有的定义为“失配后跳转的下标位置”两者之间往往差1。笔试时如果题目给了明确公式一定以题目为准如果没有给出我一般直接按“最长公共前后缀长度”的版本推导。2.2 排序算法复杂度与稳定性对照排序算法是选择题的重灾区因为可考的点太琐碎平均时间复杂度、最坏时间复杂度、空间复杂度、是否稳定、一趟排序后的序列长什么样。我用一张表把常见考点汇总一下排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性冒泡排序O(n^2)O(n^2)O(1)稳定选择排序O(n^2)O(n^2)O(1)不稳定插入排序O(n^2)O(n^2)O(1)稳定希尔排序O(n log n)~O(n^2)O(n^2)O(1)不稳定归并排序O(n log n)O(n log n)O(n)稳定快速排序O(n log n)O(n^2)O(log n)不稳定堆排序O(n log n)O(n log n)O(1)不稳定计数排序O(nk)O(nk)O(k)稳定记忆技巧可以这样排序算法是否稳定核心看是“相邻元素交换”还是“远距离跳跃交换”。冒泡、插入、归并都是相邻或分段相邻的操作相对稳定选择、快排、堆排都涉及跨越多个位置的交换容易把相等元素的相对顺序打乱所以不稳定。快排最坏情况退化到O(n^2)的原因也要记清当每次分区都极端不平衡比如序列已经有序且每次选最左端元素当基准时递归深度会变成n这时复杂度就是O(n^2)优化方法是随机选基准或三数取中。模考选择题里很爱考一类题“对某序列进行某排序后前几趟结果是什么”。这类题考的是你对排序过程的理解不是死背复杂度。比如快速排序每一趟都会让基准元素落位堆排序每一趟会把当前堆顶换到末尾。做这类题拿小例子手推一遍比背结论可靠。2.3 贪心、二分与“看似搜索”的题选择题里还有一类题不是单纯考某个算法而是给一个场景让你判断最优策略。比如区间调度问题问你按什么顺序贪心才能选最多的不相交区间。正确策略是按结束时间升序排列每次选结束最早且与已选区间不冲突的区间。为什么不是按开始时间或区间长度因为一个区间结束得越早留给后面的空间就越大这一步的贪心选择不会影响后续最优解。这就是贪心算法最核心的“局部最优能推出全局最优”的证明思路。二分也是常客。很多同学对二分的印象还停留在“有序数组里找某个数”但实际上笔试更爱考“二分答案”在一个单调的取值范围内判断某个值是否可行。比如给定若干包裹和载重量求能按时运完的最小船载量。这种题只要写出一个O(n)的check函数外面套二分复杂度就是O(n log W)同时覆盖了“搜索”和“优化”两大概念非常经典。热词里提到的粒子群、模拟退火、蚁群这类元启发式算法笔试选择题偶尔会以小场景形式出现。比如“一群候选解根据个体最优和全局最优迭代更新”你要能识别出这是粒子群算法。备考时不需要深入推导公式但要知道每个算法的核心机制粒子群靠个体极值与全局极值驱动模拟退火靠温度控制的概率接受劣解跳出局部最优遗传算法靠选择、交叉和变异。这就足够了。3. 编程题实战拆解三道最具代表性的题3.1 字符串处理题KMP的活学活用一模的第一道编程题通常是字符串处理难度不大但很考基本功。我印象比较深的一道题是这样的给定一个文本串s和一个模式串p统计模式串p在文本串s中出现的次数允许重叠。看到“允许重叠”几个字直接用string::find循环查找就会出问题因为find默认从左往右找到一个就跳过整个匹配部分。比如saaaapaa按find做法只能找到1次但实际重叠匹配能数出3次。所以这道题的正解就是KMP。完整代码如下#include bits/stdc.h using namespace std; vectorint getNext(const string p) { int n p.size(); vectorint next(n, 0); int j 0; for (int i 1; i n; i) { while (j 0 p[i] ! p[j]) { j next[j - 1]; } if (p[i] p[j]) { j; } next[i] j; } return next; } int kmpCount(const string s, const string p) { if (p.empty()) return 0; vectorint next getNext(p); int n s.size(), m p.size(); int j 0, ans 0; for (int i 0; i n; i) { while (j 0 s[i] ! p[j]) { j next[j - 1]; } if (s[i] p[j]) { j; } if (j m) { ans; j next[j - 1]; // 允许重叠匹配的关键 } } return ans; } int main() { string s, p; while (cin s p) { cout kmpCount(s, p) endl; } return 0; }这个代码里有几个点要说明。第一匹配成功时用“j next[j - 1]”而不是“j 0”目的是利用已匹配部分的信息让重叠匹配不重不漏。第二主循环里用了while(cin s p)这是牛客笔试环境的标准写法因为题目可能包含多组测试数据系统不会告诉你一共有几组只能通过读入EOF判断输入结束。3.2 图论题优先队列优化Dijkstra第二道编程题经常是图论。这次一模出现了一道很典型的最短路题n个城市、m条双向道路每条道路有长度求从起点1到终点n的最短路径长度。n和m的数据范围到了10的5次方量级这就要求算法复杂度不能高于O(m log n)也就是必须用优先队列优化的Dijkstra。朴素Dijkstra每次找最小dist时需要扫描全部节点总复杂度O(n^2)在n10^5时完全跑不动。用优先队列维护“当前dist最小的未确定节点”每次弹出并用它松弛邻居每个节点最多入队多次总复杂度降到O(m log m)就能通过。完整代码#include bits/stdc.h using namespace std; typedef long long ll; typedef pairll, int PII; const ll INF 0x3f3f3f3f3f3f3f3f; int n, m; vectorvectorpairint, ll g; vectorll dist; void dijkstra(int s) { dist.assign(n 1, INF); vectorbool vis(n 1, false); priority_queuePII, vectorPII, greaterPII pq; dist[s] 0; pq.push({0, s}); while (!pq.empty()) { auto [d, u] pq.top(); pq.pop(); if (vis[u]) continue; vis[u] true; for (auto e : g[u]) { int v e.first; ll w e.second; if (dist[v] dist[u] w) { dist[v] dist[u] w; pq.push({dist[v], v}); } } } } int main() { while (cin n m) { g.assign(n 1, {}); for (int i 0; i m; i) { int u, v; ll w; cin u v w; g[u].push_back({v, w}); g[v].push_back({u, w}); } dijkstra(1); cout (dist[n] INF ? -1 : dist[n]) endl; } return 0; }上面代码用了C17的结构化绑定牛客的编译器通常支持。如果担心环境兼容性可以把auto [d, u]换成传统的pq.top().first和pq.top().second再pop。这里有两个容易踩的坑。第一个坑是重边题目如果保证没有重边还好如果有重边更新时要取最小值。不过Dijkstra配合优先队列时即使有重边也不影响最终结果因为更小的dist会先弹出后弹出的较大dist会被vis标记直接跳过。第二个坑是数据范围边的权值和答案都可能超过int范围一定要用long longINF也要开得足够大我习惯用0x3f3f3f3f3f3f3f3f。3.3 动态规划题经典状态转移最后一类高频编程题就是动态规划。一模的DP题我遇到的是一道类似“最长不下降子序列”的变种给定一个序列求最长的子序列使得相邻元素之差不小于k。n的量级在10^5所以经典的O(n^2)DP会超时需要用贪心二分把复杂度压到O(n log n)。核心思路是维护一个数组dd[i]表示长度为i的子序列中末尾元素的最小值。遍历原序列每个元素x时在d中二分查找最后一个满足“与当前元素差至少为k”的位置然后更新。这个思路本质上是LIS问题的推广。代码可以这样写#include bits/stdc.h using namespace std; int main() { int n, k; while (cin n k) { vectorlong long a(n); for (int i 0; i n; i) cin a[i]; vectorlong long d; for (long long x : a) { // 在d中找最后一个 x-k 的位置 int l 0, r d.size(); while (l r) { int mid (l r) 1; if (d[mid] x - k) l mid 1; else r mid; } int pos l; if (pos (int)d.size()) d.push_back(x); else d[pos] min(d[pos], x); } cout d.size() endl; } return 0; }这个DP的细节比较多我实际调试时也卡过一会儿。关键在于二分边界我们希望找到一个尽量长的子序列其末尾元素可以接上当前x且满足差值条件。d数组是单调递增的所以二分找的是“最后一个 d[mid] x-k 的位置”也就是当前元素能插入的位置。更新时取min是为了让d数组保持“更小末尾优先”这样后续元素才有更大机会接上。如果k0这道题就退化成普通的最长不下降子序列二分条件变成d[mid] x代码逻辑一样。笔试中遇到这种变体题先想清楚“我能不能把它映射到学过的经典问题上”能映射就成功了一大半。4. 牛客笔试系统的使用技巧与避坑指南4.1 ACM模式输入输出处理很多第一次参加牛客笔试的同学会挂在输入输出上。牛客系统通常要求你自己处理标准输入输出也就是所谓的ACM模式。别不当回事我见过不少人在LeetCode上刷题如鱼得水一到牛客笔试反而连完整main函数都写不出来。最基础的三类输入要熟练掌握。第一类是固定数量的输入比如先给一个n然后给n个数直接cin n再循环读取即可。第二类是未知数量的多组输入系统不告诉你有几组只在每组内部以特定格式提供数据处理方式就是while(cin a b)一直读到EOF上面几道题都是这个写法。第三类是含空格的字符串输入cin s遇到空格会停如果一行里需要读取完整句子就要用getline(cin, s)。但要注意在getline之前如果用过cin n缓冲区会残留一个换行符不先用getchar()把换行符吃掉第一行getline会读到空串。这个坑我踩过不止一次。4.2 复杂度估算与数据范围判断编程题写完后必须快速估算自己的算法能不能过。我常用的判断标准是1秒内普通C代码能执行约10^8次简单操作。如果n是10^5那么O(n^2)就是10^10肯定超时必须想办法优化到O(n log n)或O(n)。反过来如果n是10^3那O(n^2)完全没问题不需要硬上更复杂的算法。空间也类似。一个int数组vector a(10^7)大约占40MB在牛客笔试常见的256MB内存限制下还可以但再来几个类似数组就危险了。开数组前先算一算别等段错误了才慌。还有一个隐性技巧如果题目给的数据范围特别大往往意味着必须用更优算法如果给的数据范围很小反而是个信号——可能可以用状态压缩、暴搜或者O(n^3)的Floyd直接过。4.3 做题顺序与时间分配关于做题顺序我的建议是倒着做不是正着做。先花两三分钟把全部题目扫一遍判断难度挑最有把握的先写。一模的编程题通常第一题最简单但有时候第二题反而是模拟题第三题才是真DP。看到题目先别急着打字先在草稿纸上把样例推一遍确认理解题意。选择题的分配也别忘了。我给自己定的规则是选择题每题最多2分钟超时就标记跳过最后如果有时间再回头。因为一道选择题的分值通常低于一道编程题的部分分为了选择题放弃编程题很不划算。编程题即使拿不到满分用暴力解法过掉部分测试用例得分也往往比空着强。5. 模考复盘与常见问题速查5.1 模考暴露出的三类典型问题复盘一模这套卷子我发现多数人的问题集中在三类。第一类是基础不牢。比如KMP的next数组定义含糊、排序稳定性记反、Dijkstra的vis标记位置写错这些都属于“见过但没真正掌握”。建议针对这类问题回到基础把每种经典算法自己手写两遍直到不看书也能在纸上推出来。第二类是代码实现速度慢。一道会做的题从想清楚思路到通过所有用例用了40分钟这在笔试里等于失败。解决方法是平时刷题时用带计时的模式练习每道题给自己设一个时限模拟真实笔试的紧张感。第三类是心态问题。看到某道题没思路心里就开始慌后面的题也没心思做。我在模考时也遇到过这种状态后来总结出一条原则先拿能拿的分。如果一道题没有AC思路立刻把暴力写法写上至少拿部分分然后再去想优化。5.2 高频报错与排查方法速查表我把这次模考以及平时帮学弟学妹调试时遇到的高频问题整理成一张速查表笔试前可以快速翻阅报错/现象可能原因排查思路运行超时算法复杂度过高、死循环把n的范围套进复杂度估算检查循环跳出条件答案错误边界条件遗漏、未取模构造极端数据空输入、最大n、全是相同元素段错误访问越界、栈溢出检查数组下标范围递归改循环输出格式错误多打印空格/换行严格按照样例输出行末不要留多余空格编译错误头文件缺失、变量重名本地编译一次再提交优先用万能头5.3 一模之后的复习路线建议一模结束后的复习不建议再盲目刷题而是按“查漏补缺—专题训练—模拟冲刺”三步走。查漏补缺阶段把模考中做错的题、蒙对的题全部整理出来分析是哪个知识点薄弱。专题训练阶段针对薄弱点集中刷30到50道同类型题目比如数据结构薄弱就集中刷链表和二叉树图论薄弱就集中刷最短路和拓扑排序。模拟冲刺阶段考前一周每天做一套牛客模考或真题重点训练时间分配和临场心态。如果时间还充裕可以适当了解一些扩展算法比如粒子群、模拟退火、卡尔曼滤波、BM25、PID控制等。它们的原理不复杂但在面试聊项目时能体现知识广度。最后说一点我自己的体会。参加一次牛客模考一模算法笔试最大的价值不在于分数而在于让你在真正上考场前把所有容易犯的错都提前犯一遍。我当年就是一模做得稀烂才下定决心把KMP、Dijkstra、DP这些经典算法逐一手写了一遍后来二模三模成绩明显提升。所以如果你一模没考好别灰心把它当成一次免费的实战演练。问题暴露得越早你能补救的时间就越多。
返回列表