牛客模考算法笔试复盘:从时间分配到KMP与动态规划实战策略

发布时间:2026/8/31 4:31:58
牛客模考算法笔试复盘:从时间分配到KMP与动态规划实战策略 第一次做2023年牛客模考一模的算法笔试我是抱着“测测自己水平”的心态去的。当时LeetCode刷了两百多题剑指offer也过了两遍自认为常见的栈、队列、二叉树、动态规划都能手写出来。结果模考的成绩相当打脸四道题AC了两道半剩下那道题连题面都没读完。更让我难受的不是分数低而是它暴露出了一个平时刷题根本发现不了的问题——我在限时、封闭、没有题解可查的环境里做题节奏和取舍能力一塌糊涂。所以这篇内容不打算复述具体题目也不做所谓的“押题”而是想聊聊参加牛客模考这类算法笔试到底考察的是什么考前该准备什么考试中如何分配时间考后如何把错题变成自己的能力如果你正准备算法岗的实习或校招笔试或者想找一个检验自己算法水平的方式这篇总结应该对你有参考价值。1. 为什么我建议算法岗同学都去参加一次牛客模考1.1 模考和平时刷题的本质区别平时刷题是一个“可以随时暂停”的游戏。卡住了可以翻题解困了可以歇会儿提交错了可以马上看错误用例。这种状态下你练的不是真实笔试能力而是“在宽松环境下解决问题的能力”。而牛客模考模拟的是真实笔试场景从开考开始计时中途不暂停提交错误只能根据提示调整整个过程中没有参考答案。压力水平完全不一样决策模式也完全不一样。我用一个表格来对比一下维度平时刷题牛客模考/真实笔试时间自由可中断严格计时不可暂停辅助可看题解、搜资料只能依赖自己选题按标签刷从易到难混合题型难度随机反馈提交后看用例、改Bug有限反馈靠推理和调试心态放松有压力容易焦虑这两种模式练的其实是两种能力。第一种是“能不能解决某个算法题”第二种是“在有限时间内最大化得分”。牛客模考一模让我清醒地认识到我平时练得更多的是第一种而真实笔试里决定胜负的往往是第二种。1.2 一模的题型构成与考察方向从牛客模考的常见出题方向来看算法笔试的核心板块集中在几块数组与字符串、排序与二分、贪心、动态规划、图论与搜索、数据结构设计。我在一模里遇到的核心题目基本没有跳出这个范围。很多同学容易陷入一个误区觉得算法笔试考的是“偏题怪题”。实际上从牛客平台的历年模考和笔试题库看高频考点依然是那些经典问题KMP算法这几乎成了字符串匹配题的标配考点一模里就有一道要求理解next数组的题。动态规划背包、最长递增子序列、编辑距离这些经典模型反复出现。贪心 排序区间调度、任务安排类的题目核心是在排序规则上做文章。图论Dijkstra最短路、BFS求连通块、拓扑排序都是需要手写模板的题目。多提一句像粒子群算法、模拟退火这类元启发式算法在平时看论文和做科研时可能经常接触但算法笔试里极少考查因为评测系统很难对这类随机化算法做稳定判题。备考时间有限的情况下不建议优先花时间在这些内容上。2. 考前48小时我做了什么环境与知识体系检查2.1 编程环境、输入输出模板的提前准备模考用的评测系统是牛客网的标准在线OJ支持多种语言。我在一模前犯了一个低级错误以为自己很熟C就没做任何环境准备结果在输入输出上浪费了十几分钟。真实的算法笔试尤其是大厂机试很多是ACM模式也就是要自己处理输入输出而不是LeetCode那种已经封装好的核心函数模式。提前准备几套趁手的输入输出模板看着像小事实际节省的时间非常可观。下面分享我常用的C模板#include bits/stdc.h using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin T; // 多组数据 while (T--) { int n; cin n; vectorint a(n); for (int i 0; i n; i) { cin a[i]; } // 业务逻辑 } return 0; }Python的话我建议直接使用sys.stdin.read()读取全部数据然后按索引切片这样比一行一行input()要快很多尤其在输入量大的题目里差别非常明显import sys def solve(): data sys.stdin.read().strip().split() if not data: return idx 0 t int(data[idx]); idx 1 for _ in range(t): n int(data[idx]); idx 1 arr list(map(int, data[idx:idxn])) idx n # 业务逻辑 pass if __name__ __main__: solve()这些模板应该提前在本地环境调试好而不是考试时现场回忆。另外注意把语言版本、编译选项、内存限制这些信息提前看清楚。有些同学考试时才发现自己选的语言不支持某些语法特性那种慌乱会直接影响后面整场发挥。2.2 高频考点清单从排序到动态规划考前我把高频考点整理成了一份清单相当于“知识地图”。你不是非得把所有算法都练到精通但至少要保证每个高频考点都有印象、会写模板、知道复杂度。这是我的清单数据结构基础数组、链表、栈、队列、哈希表、堆、并查集、线段树了解基础算法排序快排、归并、二分查找、双指针、滑动窗口、前缀和与差分字符串KMP、Trie可选的、字符串哈希搜索DFS、BFS、回溯、剪枝动态规划线性DP、背包、区间DP、状态压缩进阶图论建图、Dijkstra、Floyd、拓扑排序、最小生成树Kruskal为什么这几项是“必会项”因为它们几乎是算法笔试的骨架。比如二分不一定直接考“在有序数组里找目标值”更多是考“最小化最大值”这类二分答案的逻辑。KMP也不一定直接问你next数组是什么但字符串匹配的题目用朴素算法会超时此时能想到KMP就代表着分数差距。这里有个经验考前一天不适合学新算法。我见过有人在模考前一天开始啃后缀自动机结果第二天基础题反而状态全无。考前应当做的是把已经熟练的模板默写一遍把容易忘的边界条件确认一遍比如并查集的路径压缩、Dijkstra的优先队列写法而不是硬记新东西。3. 正式考试时的做题顺序与时间分配策略3.1 先通读题目标记难度梯度一模开考后我犯的第二个错误是按题目顺序一题一题做结果第一道偏难的题卡了快40分钟后面能拿分的题只能仓促收尾。后来我调整了策略效果立竿见影。正确做法是开考后的前5分钟把所有题目全部读一遍在草稿纸上标记每道题的难度感知、预计题型、大致分值。这个过程看似“耽误时间”实际是在给自己的大脑建立全局地图让你知道哪些题是送分题哪些题是保底题哪些题是可以放弃的题。我一般会用四个等级做标记S级一眼能看出思路的题先做。A级有思路但需要仔细推导的题第二顺位做。B级有模糊思路但复杂度高、细节多的题在保证前面完成后尝试。C级完全没思路的题放到最后或者直接放弃。这种分级不需要很复杂核心是避免“深陷一道题错过整场考试”。很多真实的算法笔试是“按通过的测试点比例给分”所以拿到部分分数比完整AC一道难题更重要。某道题能暴力过50%的测试点那就应该先暴力拿分留到最后再想优化而不是一开始就硬冲满分解法。3.2 分值收益导向的做题顺序在实际笔试里每道题的分值并不一定相同通常会有难度和分值的对应关系。在没有明确分值的模考里我采用“简单题优先 中档题保量 难题最后”的原则。拿一次四题模考举例时间安排大致如下题目类型预计用时策略第1题 简单模拟/基础题15-25分钟必须AC写完后多测几组边界用例第2题 数据结构/搜索30-40分钟争取AC如果有困难先写暴力保证部分分第3题 动态规划/贪心30-40分钟能够推导出状态转移再做否则跳过不纠结第4题 综合难题剩余时间拿下基础测试点不恋战注意这个时间表不是死的。如果你做题较快中档题可以再分配多一些时间如果你发现某道题30分钟还没思路果断标记为“待定”跳到下一题。这在心理上也是一种减负——你不是在一道题上“失败”而是在为一个还没有拿到的分数争取时间。我自己还有一个习惯**把所有代码的草稿先写在纸上再用键盘敲或者先用注释写伪代码再填充细节。**因为考试期间思路容易中断如果直接在编译器里一边想一边敲很容易陷入不停删除重写、头脑混乱的状态。先用注释搭好框架再逐行实现往往能大幅提高代码一次性通过的几率。4. 四类高频题型的实战破题复盘4.1 字符串与模式匹配题目KMP的next数组到底怎么用一模里最让我印象深刻的是字符串匹配相关的考察。其实题目本身并不离谱但如果你只会暴力匹配在面对长字符串时就会超时。KMP很多人都“背过”但真正到了写代码的时候next数组的边界条件经常会出错尤其是next[i]的定义。简单回顾一下在KMP算法中对于模式串Pnext[i]通常表示当P[i]匹配失败后指针应该回退到的位置即P[0...i-1]的最长相等前后缀长度。计算next数组的过程本身也是一个“匹配”过程关键是理解j next[j]这条回退规则。一个标准的KMP next数组计算模板如下vectorint get_next(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; }这里有个细节很多人会混淆next[i]指的是P[0...i]的最长相等前后缀还是P[0...i-1]的。不同教材定义不同考试时如果没有明确说明建议写代码前先在注释里定义清楚避免自己把自己绕晕。牛客模考这类在线OJ只看输出结果只要你按自己的定义把逻辑写对就行但如果你一边用next[0]-1的版本一边又用P[0...i-1]的处理方式就会越写越乱。除了KMP字符串哈希也是个实用的后备方案。在处理多个模式串匹配、或者需要快速判断两个子串是否相等时哈希配合二分可以解决很多KMP不好扩展的问题。不过哈希碰撞问题需要注意模考中一般不会刻意构造哈希冲突数据但平时练习时还是要谨慎。4.2 动态规划题状态定义是生死线一模的动态规划题我“会但没写对”原因就是状态定义一开始就错了。DP题最核心的不是状态转移方程怎么写而是状态定义是否准确。状态定义错了后面一切推导都是空中楼阁。以一个经典的“最长递增子序列”为例错误定义dp[i]表示前i个元素中最长递增子序列的长度。这样定义后你根本不知道上一个元素选了谁无法判断下一个元素能不能接上。正确定义dp[i]表示以nums[i]结尾的最长递增子序列的长度。为什么必须“以i结尾”因为“递增子序列”是一个有顺序概念的子序列必须以某个元素作为结尾才能推导下一个元素是否合法。这类经验可以推广到几乎所有线性DP题目如果转移时你需要知道“上一个选择的元素是谁”那状态里就一定要包含“当前选择以谁结尾”这个信息。一个比较稳妥的DP做题流程是先想暴力递归把问题的朴素递归写出来。找出递归函数的状态参数这往往就是DP状态。把递归改成记忆化搜索理清状态依赖关系。把记忆化搜索改成自底向上的DP确定遍历顺序。很多人在考场上直接“想状态、写转移”结果因为边界条件反复修改浪费大量时间。而先从暴力递归入手虽然多写几行但思路清晰、不容易漏状态。遇到实在推不出DP转移的题退回到暴力搜索拿部分分也远比死磕DP要好。4.3 贪心与排序类题目证明比猜测更重要贪心题是算法笔试里最容易“觉得对”的一类题。很多同学看到题目后凭直觉排个序觉得“差不多就是这样”结果提交后WA了也不知道哪里错。一模里我就掉进过这种陷阱。比如经典的“会议室安排”问题给定多个区间的开始和结束时间选择最多数量的不重叠区间。正确的贪心策略是按结束时间升序排序然后依次选择不重叠区间。但如果你按开始时间排序或者按区间长度排序在某些数据下会得出错误答案。为什么一定要按结束时间因为“结束时间越早给后面留下的时间就越多”这是可以通过交换论证证明的。所谓交换论证就是假设最优解与贪心解在某个选择上不同然后通过交换选择顺序证明贪心解不会比最优解差。考场上不可能每一步都做严格证明但至少要能在头脑里构建一个“反例测试”我的贪心策略在什么情况下会失败我总结的贪心题三步走第一步尝试用一个局部最优策略去模拟样例看是否成立。第二步构造边界反例比如随机生成小规模数据暴力验证贪心策略。第三步如果实在无法验证就先写一种敢赌的贪心拿到部分分数不要在一道题上耗太久。另外排序类题目经常不是单纯考排序API而是考“排序的键怎么定义”。比如任务调度中如果同时有截止时间和收益排序规则可以是“收益降序”外加“尽量靠后安排”。这种复合排序思路需要平时多积累考试时现场想往往会慢人一步。4.4 图论与搜索题目建图方式决定复杂度图论题在一模里出现的概率非常高。很多同学不是不会Dijkstra或BFS而是不会“建图”。比如一个二维网格问题要把每个格子当作一个节点网格中相邻的格子连边而一个“依赖关系”题目需要建出有向图再跑拓扑排序。建图方式直接影响复杂度。以最短路为例节点数量少比如n ≤ 500可以用邻接矩阵简单直观写Floyd也行。节点数量多比如n ≤ 1e5必须用邻接表堆优化的Dijkstra才能跑到O((nm)logn)。我建议在真正动手写图算法前先明确几件事图是有向还是无向是否有负权边是否有重边和自环是否需要处理连通分量这些信息决定了你选BFS、Dijkstra还是SPFA也决定了建数组存边的具体写法。给你一份我常用的带权邻接表建图模板Cvectorvectorpairint, int graph(n); for (int i 0; i m; i) { int u, v, w; cin u v w; --u; --v; // 如果输入是1-indexed graph[u].push_back({v, w}); graph[v].push_back({u, w}); // 无向图 }Dijkstra模板堆优化版考试时我建议直接背到肌肉记忆vectorlong long dijkstra(vectorvectorpairint, int graph, int s) { int n graph.size(); const long long INF 1e18; vectorlong long dist(n, INF); priority_queuepairlong long, int, vectorpairlong long, int, greater pq; dist[s] 0; pq.push({0, s}); while (!pq.empty()) { auto [d, u] pq.top(); pq.pop(); if (d dist[u]) continue; for (auto [v, w] : graph[u]) { if (dist[v] dist[u] w) { dist[v] dist[u] w; pq.push({dist[v], v}); } } } return dist; }之所以强调模板熟练是因为图论题在调试时的“不可见性”很高。数组越界、重边导致更新问题、priority_queue默认大顶堆导致忘记传greater——这些都是细碎的错误但每个都可能让你浪费20分钟。5. 从“会做”到“AC”代码实现层面的细节坑5.1 ACM模式下的输入输出陷阱很多LeetCode刷题党第一次参加牛客模考时最明显的痛点是输入输出。LeetCode已经把参数封装好了而在牛客模考这种ACM模式环境下一切都要自己处理。常见陷阱包括多组数据有些题目没有说明“只有一组数据”实际上会循环输入直到EOF。如果你只跑一次就退出就会出现只通过部分测试点的情况。输入行尾有多余空格使用cin 或split()一般没问题但如果你用getline按行读就要注意空行和行尾空格。输出格式要求输出两行却只输出一行或者要求用空格分隔却输出成换行。这些在样例里能看到但样例能覆盖的场景非常有限必须自己脑补所有输出边界。我自己的习惯是在写好核心逻辑后先手工构造三组测试用例最简输入、随机中等规模输入、极端边界输入。比如n1、n0、数组全相同、数组已升序、数组逆序。这些用例能在提交之前拦截大量低级错误。5.2 边界条件与特殊用例Algorithms笔试的判题系统很“诚实”边界用例过不去就是过不去。最经典的几个坑我每次做模考都会提醒自己一遍索引越界C的vector访问越界不会报错而是产生未定义行为结果可能是WA、TLE或RE。写循环时习惯性地检查i1 n。整数溢出当数据范围在1e9级别时相加可能超过int范围。建议在涉及累加、乘法的题目中直接用long long不要为了省事用int。二分死循环while (l r)和状态更新方式必须配套。一种稳妥写法是while (l r) { int mid l (r - l) / 2; if (check(mid)) r mid; else l mid 1; }这个模板处理“寻找第一个满足条件的值”时不会死循环。如果你用l mid的形式mid要做成l (r - l 1) / 2否则可能在区间长度为2时陷入死循环。5.3 复杂度估算与优化时机在写题前先看一眼数据范围能帮你决定是否可以直接暴力。如果你面临的是n 20那么指数级搜索大概率可行如果n 1e5O(n^2)几乎必挂必须想O(nlogn)或O(n)的算法。我一般在草稿纸上快速估算n1e5O(n^2) 1e10不可行n1e5O(nlogn) ≈ 1.7e6完全没问题n1e6O(n) 刚好n1e9只能O(logn)或O(1)这种估算能避免你在暴力代码上浪费感情。如果一开始就知道O(n^2)不可行直接转向优化算法。但有一种情况例外你推不出优化算法那也别直接放弃——先把O(n^2)的暴力写了提交看能过多少测试点有些题的数据分布是“小数据给分”暴力也能拿40%或50%的分数。这个策略在真实笔试里非常重要。6. 模考之后如何把错题变成能力6.1 建立错题归因模型考完模考后很多人只看一眼分数就过去了这是最浪费的行为。模考的精华在于错题但错题不是“重做一遍”就结束了还要搞清楚“为什么错”。我给自己设计了三个归因类型你可以直接用归因类型典型表现应对策略知识盲区题目用到我从没听说过的算法或模型把这套算法补充到知识地图里找3-5道同类题练习思路受限会那套算法但没识别出这道题应该用它训练“题目关键词→算法”的映射比如“最小最大问题”优先想到二分答案实现不稳想清楚了但写出来不断WA/TLE强化代码模板重点练习边界用例和输入输出一模结束后我把自己所有错题都放进这三个类型里。最后发现我的主要问题不是知识盲区而是“思路受限”和“实现不稳”——说白了就是做题数量和真实场景练习不够。这让我后续的刷题计划更有了针对性。6.2 针对弱项的专项训练策略知道了短板在哪接下来就是有针对性的训练。这里分享一个我后来一直沿用至今的训练方法按专题刷题而不是按题库顺序刷。比如花一周时间只刷二叉树的题目把前序遍历、中序遍历、后序遍历、层序遍历、最近公共祖先、树的直径等全部打通再进入下一个专题。定时模拟。每周固定抽出2小时找一套牛客模考题或网上搜的往年真题严格按考试时间走模拟考试状态。只有在这种定时训练下你才能在真实考场上对“什么题该花多少时间”有感觉。写题解尤其是记录错误。每次WA后不要急着看题解先把错误用例记录下来思考为什么你的逻辑没有覆盖这个用例。这一步比做对题目本身更重要。我个人的体会是一模后两周的针对性训练比之前一个月漫无目的的刷题效果更好。原因很简单**漫无目的刷题是用时间换熟悉度而基于模考的复盘是用数据换精准度。**你知道自己哪里会错练习时就会主动去碰那些“不确定”的边界而不是反复做自己已经会的题。最后再分享一个小技巧每次模考后把AC代码和没用上的模板整理成一个“考试工具箱”文件夹。二模、三模前我只花半小时把这个文件夹过一遍重点看那些当时想不起来、卡壳过的模板。这比从头到尾翻笔记强很多。算法笔试拼到最后不完全是智商和灵感更大程度上拼的是你对自己知识边界的掌握程度以及你在时间压力下做取舍的果断程度。希望这篇复盘对你准备下一次模考或正式笔试有所帮助。