ARTICLE DETAIL

资讯详情

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

牛客模考三套题全解析:从双指针到动态规划的笔试通关思路

牛客模考三套题全解析:从双指针到动态规划的笔试通关思路 2018年秋招那阵子刷题圈里流传着一句话牛客模考的三套题能刷完并复盘明白的人笔试阶段基本不会太慌。我当时对“三模”这套编程题集感触特别深因为它不像很多OJ题库那样只堆知识点而是把数组、字符串、动态规划、搜索、贪心这些高频考点揉进一场模拟笔试里难度梯度非常接近真实校招。现在虽然过去好几年但这类命题思路仍然被大量公司沿用把核心考点和踩坑经验整理出来对正在准备笔试的朋友依然有直接参考价值。这篇文章不会贴完整原题描述我把考点、通用解法和调试路径讲清楚你拿来就能用。1. 题集全貌2018三模为什么值得反复刷1.1 题集背景与考点分布牛客模考是校招季的在线模拟笔试三模一般指的是第三次模拟场次。2018年那次三模的编程题集合给我的第一印象是“不偏”。它没有那种只有竞赛选手才做得出来的冷门数学题也没有纯背诵性质的题目绝大多数题都能归到这几类数组与线性表操作包括去重、合并、区间处理、双指针扫描动态规划尤其是线性DP和简单背包问题字符串处理与模拟考察对边界条件的敏感度图或状态搜索包括BFS、DFS以及回溯贪心策略需要先分析局部最优和全局最优的关系。这一点很有参考价值。真实笔试不是让你证明某个冷门定理而是看你在有限时间内能否识别题型并调用熟悉的解法。三模这套题集的考点分布几乎就是当年校招笔试的缩小版。1.2 难度梯度与命题风格套题里通常会有两三道送分题比如简单的数组遍历、字符串反转、排序后取值这类题考察的是基础API熟练度和代码速度。中间档会有一两道需要绕弯的题不复杂但容易忽略条件比如“数组中有序去重”这类解法不长但双指针的思路要能想到。压轴题则偏向动态规划或搜索直接写暴力容易超时需要进一步优化。命题风格上牛客的题普遍偏“工程化”不像部分OJ那样只给函数签名它要求你自己处理输入输出所以提交前必须想清楚怎么读数据、怎么处理多组用例。这也是很多从LeetCode起步的选手一到牛客笔试就白屏的主要原因。1.3 为什么今天仍值得复盘2018年的题在算法层面并不会过时。校招笔试的考点迭代很慢双指针、DP、搜索、贪心到今天都是主流。更重要的是这套题集的难度系数比较适合校招中前期比基础刷题阶段难又比顶级大厂笔试的压轴题温和。用它来检验自己的“笔试状态”比如能否在60分钟内AC中等难度题是很有效的自测方式。2. 高频考点逐个拆双指针、DP、字符串、搜索、贪心2.1 数组与双指针一个模板解决一批题三模题集里数组操作出现频率极高几乎每场笔试都会至少有一道。这类题有个共性如果能用两层循环解决就要警惕是否需要优化成双指针。我印象很深的一道同型题是有序数组去重要求原地删除重复元素并返回新长度。很多人一上来就用set确实能过但面试官其实更想看你能不能写出原地版本。双指针写法非常固定def remove_duplicates(nums): if not nums: return 0 write 0 for read in range(1, len(nums)): if nums[read] ! nums[write]: write 1 nums[write] nums[read] return write 1这个模板里read负责遍历write负责维护结果区的末尾。因为数组有序重复元素一定连续所以不需要额外空间。同理合并两个有序数组、判断回文、三数之和等题核心都是“移动哪一边、什么时候停”。写这类题的时候我给自己定了一个原则先明确两个指针的语义再写循环。很多WA不是思路错而是指针移动条件写反或者没处理空数组和单元素数组。2.2 动态规划先写递归再改递推动态规划在2018三模题集里属于压轴常客。典型考点是最长递增子序列、爬楼梯/硬币组合、求最大子段和这类。第一次做DP题最容易犯的错是直接背状态转移公式却不理解状态从哪里来。以最长递增子序列为例先别管优化写一个最直观的递归或记忆化把“以i结尾的LIS长度”这个状态定义清楚def length_of_lis(nums): if not nums: return 0 dp [1] * len(nums) for i in range(len(nums)): for j in range(i): if nums[j] nums[i]: dp[i] max(dp[i], dp[j] 1) return max(dp)这个O(n^2)版本能够通过一部分测试用例笔试时也算保底分。想拿全分就需要知道LIS还有O(n log n)的贪心二分写法。我在复盘时常说一句话DP题的进阶不是上来就写最优解而是先保证暴力递推正确再逐步优化。笔试场上尤其明显。如果一道DP题你五分钟内没有理清递推关系先写一个能跑出小数据正确结果的递归版本再拿它当benchmark去验证优化版。这个习惯帮我避免了很多次“优化完反而WA”的尴尬。2.3 字符串与模拟边界条件就是得分点字符串题看起来简单实际是丢分重灾区。三模题集里有很多“给定两个字符串实现大数相加”“判断括号是否匹配”这类模拟题重点在边界。以大数相加为例核心是处理长度不一致和进位def add_strings(num1, num2): i, j len(num1) - 1, len(num2) - 1 carry 0 res [] while i 0 or j 0 or carry: x int(num1[i]) if i 0 else 0 y int(num2[j]) if j 0 else 0 total x y carry res.append(str(total % 10)) carry total // 10 i - 1 j - 1 return .join(reversed(res))这道题我当年WA过一次原因是while循环里漏掉了carry不为0的情况比如999 1最高位会凭空消失。做字符串模拟题我习惯把所有边界用例先列出来空串、最前面有0、全是9、长度差很大。边界全过AC基本稳。2.4 搜索回溯记住状态和剪枝搜索题在三模里不一定每场都有但一旦出现通常都是区分度题。常见题型包括迷宫最短路径、矩阵中的岛屿数量、全排列、组合总和。BFS的重点是层序遍历要配合visited数组DFS的重点是状态恢复。写回溯时有一个通用框架def backtrack(path, remaining): if not remaining: # 记录结果 return for i, item in enumerate(remaining): backtrack(path [item], remaining[:i] remaining[i1:])这个框架能解决全排列但性能很差。实战中一定用布尔数组标记已选元素避免反复切片。另外搜索题最容易超时的场景是忘记剪枝比如组合总和里如果当前和已经超过目标值可以直接return不需要继续深入。我在复盘三模搜索题时最大的体会是搜索题不要追求花哨技巧先保证“状态不重不漏”再考虑剪枝。剪枝顺序也很重要通常先剪掉明显非法分支再考虑对称性剪枝或记忆化。2.5 贪心证明比实现重要贪心题的实现通常很短难点在于“为什么这样选是对的”。三模题集里的贪心题多以区间调度、活动安排、糖果分配这类经典形式出现。最常见思路是按结束时间排序然后每次选结束最早且不冲突的区间。比如活动安排问题intervals.sort(keylambda x: x[1]) count 0 end float(-inf) for start, stop in intervals: if start end: count 1 end stop这类题AC很容易但面试或复盘时要能说出理由按结束时间排序能保证剩余空间最大化因此是全局最优。一旦在笔试里判断出是贪心优先想排序规则而不是急着写循环。3. OJ提交时最容易翻车的几个细节3.1 牛客式输入与LeetCode式输入的区别很多刷题主力是LeetCode起家的习惯了只写函数体不需要管输入。但牛客模考这类赛制要求把整个程序写完包括读入、解析、输出。三模第一题如果不识别多行输入往往直接卡住。常见的多行读取模板这样写import sys def solve(line): data line.strip().split() # 处理一组数据 return str(result) for line in sys.stdin: line line.strip() if not line: continue print(solve(line))注意strip()能去掉换行和末尾空格split()会按任意空白切分。如果题目说“每组测试数据第一行为N”千万不要假设测试组数只有一组。3.2 数据范围决定算法走向编程题集合里从来不缺“看起来很简单但一交就超时”的题。看到数据范围先做复杂度估算n 10O(n!)都可以n 1000O(n^2)通常没问题n 10^5需要O(n log n)或O(n)n 10^6严格线性。我之前有一道题用O(n^2)写得很顺一提交TLE后来才发现n的范围是10^5。这个教训让我养成了读题先看范围的习惯比写代码更重要。3.3 多组测试数据与EOF的坑三模题集里有的题没有明确告诉你有多少组数据只说“输入到文件结尾”。这种情况需要循环读。还有一个隐藏坑是数据分布某些题目中中间可能夹杂空行如果直接用int(input())会抛异常必须加空行判断。处理多组数据时我常用一个错误示范提醒自己# 这种写法可能报错因为输入行可能为空 n int(sys.stdin.readline().strip())正确做法是先判断读出来的字符串是否为空再做转换。这个细节看似简单但在“输入若干行整数每行以空格分隔”这种题目里非常容易翻车。4. 从TLE到AC一道典型题的优化全过程4.1 先写暴力解法拿部分分三模题集里有一类“查找数组中满足某种条件的数对”的题最自然的解法是双重循环。暴力版本不仅正确率高还能帮助理解题意。比如寻找两数之和等于target的下标def two_sum(nums, target): n len(nums) for i in range(n): for j in range(i 1, n): if nums[i] nums[j] target: return [i, j]这个版本在n很小时能过但在大规模数据下必然TLE。如果你在笔试中提前完成了简单题可以用暴力版本先拿部分分再逐步优化不会手忙脚乱。4.2 哈希表优化空间换时间优化的核心是消除内层循环。把已经遍历过的值存到哈希表里判断target - nums[i]是否存在def two_sum(nums, target): seen {} for i, num in enumerate(nums): diff target - num if diff in seen: return [seen[diff], i] seen[num] i return []时间复杂度从O(n^2)降到O(n)代价是额外O(n)空间。笔试中这种“空间换时间”的套路非常常见尤其是在数组和字符串相关题目中。4.3 再进一步排序与双指针如果题目要求返回数值而不是下标还可以先排序再用双指针从两端往中间扫描。对于更常见的三数之和排序加双指针比三重循环更实用def three_sum(nums): nums.sort() res [] n len(nums) for i in range(n - 2): if i 0 and nums[i] nums[i - 1]: continue left, right i 1, n - 1 while left right: total nums[i] nums[left] nums[right] if total 0: res.append([nums[i], nums[left], nums[right]]) while left right and nums[left] nums[left 1]: left 1 while left right and nums[right] nums[right - 1]: right - 1 left 1 right - 1 elif total 0: left 1 else: right - 1 return res这里的去重逻辑很容易出错我当年就在nums[i] nums[i - 1]这行吃过亏。复盘时最大的心得是这类题不要背代码要理解“为什么排序后可以跳过重复值”。理解了之后遇到四数之和也能改。5. 复盘这套题之后我对笔试准备的三点调整5.1 不再盲目刷题按错误类型建错题本刷完三模后我对比自己AC和WA的题目发现错误高度集中边界条件、输入解析、复杂度预估。原来的刷题方式是按题号做后来改成按错误类型归类。错题本里不再是简单抄题而是记录三列错误现象、根因、下次怎么避免。比如“输出格式错误”那一栏写的是“先把所有输出拼成字符串最后再print避免多行输出中间加了多余空行”。这个方法让我在后续笔试里少踩了很多同样的坑。5.2 考场时间分配暴力分也是分三模难度逼近真实笔试我第一次模拟时总想着每题都AC结果在压轴题上耗了40分钟前面的题目反而没时间优化。后来我调整策略前5分钟快速扫完所有题区分送分题、中等题、压轴题送分题15分钟内拿下中等题如果20分钟内没有完整思路先写暴力版本保底压轴题最后处理最多留30分钟卡住就及时停。这个策略的核心是“保证每道题都有产出”。笔试看的是总分不是看你会不会做压轴题。5.3 手写代码的规范性小细节决定大成败牛客模考这类笔试环境有些没有自动补全变量名写得太随意会导致后面自己都看不懂。我在复盘时养成了几个习惯变量名用有语义的词比如write_index而不是w循环变量尽量短但只在局部使用复杂逻辑旁边写一行注释说明“这里为什么这么做”提交前先跑一遍自己构造的边界用例。尤其最后一点我常用用例包括空数组、单元素数组、全相等数组、最大规模数组。单测不一定要写成文件但是心里过一遍并不难。很多WA的题再读一遍题干就能发现是自己漏了条件而不是算法本身有问题。我自己的体会是2018牛客模考三模这套题集真正宝贵的不是题目本身而是它逼着我把刷题状态从“追求AC数量”调整成了“关注稳定性和复盘深度”。如果你现在刷题遇到瓶颈不妨找一套难度适中的模拟题限定时间完整做一遍再按上面的方式复盘一次。这个过程比单纯多做几十道题更有效。
返回列表