ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

2019牛客三模编程题解析:栈、滑动窗口、动态规划与贪心实战

2019牛客三模编程题解析:栈、滑动窗口、动态规划与贪心实战 秋招季刷题的人应该都绕不开牛客的模考系统2019年那场三模编程题我到现在印象都挺深的。倒不是说题目有多难而是这套题的出题思路很有代表性——它基本就是校招笔试的题型风向标覆盖了栈、字符串、动态规划、贪心这几个高频考点而且难度梯度安排得比较合理从签到题到压轴题都有。当时我刷完这一套题去参加几家大厂的笔试发现很多题目在思路上都有相似之处。这篇文章我打算把2019牛客三模编程题里最有代表性的几道题拿出来逐题拆解思路、写出完整的Python实现再聊聊我在实际手写代码和提交过程中踩过的坑。无论你是正在准备校招笔试的应届生还是想系统复习数据结构的在职开发这套题的训练价值都很高。我会尽量把每个思路的来龙去脉讲清楚而不是直接甩一段代码让你背。1. 2019牛客三模编程题整体情况与考点分析1.1 这套题的题量与难度设计2019牛客三模编程题一共四道整体风格贴近互联网公司校招技术岗笔试的常见设定。四道题分别考查了基础数据结构操作、字符串处理能力、动态规划状态设计以及数学建模与贪心策略。从实际做题体验来看前三道题属于“如果复习过核心算法就能稳定拿分”的范畴第四道题的思维门槛会高一些需要跳出常规套路去分析问题的本质。这种难度梯度不是巧合而是模拟了真实笔试的筛选逻辑。公司笔试要区分“基本编程功底”和“算法思维上限”两个维度所以一定会安排一道拉不开差距的送分题再安排一道让人卡住的题。如果你在练习时发现某道题怎么也过不了不用太焦虑先确保自己把该拿的分都拿到再回头攻坚压轴题这个策略在真实笔试里同样适用。1.2 高频考点的分布逻辑与刷题方向从这套题出发我们可以提炼出校招笔试里出现频率最高的几类考点栈与队列应用包括括号匹配、表达式求值、单调栈等变体字符串与滑动窗口子串问题、字符计数、双指针移动动态规划线性DP、背包类问题、状态压缩的入门思路贪心与数学排序后按某种策略选择通常是压轴题的常客很多同学刷题喜欢按“数据结构”分类刷比如这周只刷栈下周只刷字符串。这个思路没问题但到了模考阶段一定要切换到混合模式因为真实笔试不会告诉你这道题该用什么数据结构你得自己判断。2019牛客三模的价值恰恰就在这里它不是帮你巩固某一个知识点而是逼你在有限时间内完成“识别题型—选择算法—写出代码—调试通过”的完整链路。2. 核心题型拆解从题意到算法选型2.1 第一题栈与模拟最容易被忽视的送分题第一题是比较典型的“括号匹配”变体要求在给定字符串中判断括号是否合法同时支持大小写字母和数字混入。很多同学觉得这类题简单上手就写结果提交之后发现样例能过但隐藏用例挂了原因基本都出在“没有处理空栈”“匹配完还剩左括号”这类边界情况上。这道题的标准解法是用一个栈存左括号遇到右括号时弹出栈顶元素做匹配检查。Python里我们用list就能实现栈append入栈pop出栈时间复杂度O(n)空间复杂度也是O(n)。整体过程是stack [] pairs {): (, ]: [, }: {} for ch in s: if ch in ([{: stack.append(ch) elif ch in )]}: if not stack or stack[-1] ! pairs[ch]: return False stack.pop() return len(stack) 0回头检查一下代码最后那个len(stack) 0特别关键。如果括号都能匹配相邻的但最后栈里还剩左括号说明字符串不合法比如((()))(这种前面六个字符都能匹配但最后一个左括号落单了。肉眼看不出来提交时用例会用这种输入来测试你是否考虑周全。我试过把这段代码改成只用一个变量计数代替栈遇到左括号加一遇到右括号减一最后看计数器是否为零。对于纯括号匹配这种写法是可行的但这个版本无法区分不同括号类型一旦出现([)]这种交叉嵌套计数器法就会误判为合法。所以如果题目里有多类括号老老实实用栈不要自作聪明。2.2 第二题字符串处理与滑动窗口的双指针技巧第二题考查的是字符串中最长连续不重复子串的长度这是滑动窗口题型的经典代表。题目给定一个字符串要求找出其中不含重复字符的最长子串长度返回长度值即可。核心思路是维护一个窗口窗口内保证没有重复字符窗口右边界不断扩展左边界根据情况收缩。具体来说我用一个字典last_pos记录每个字符最近一次出现的位置右指针从0遍历到n-1。每次遇到一个字符先看它是否已经在字典里如果已经在说明当前窗口内出现了重复需要把左指针移动到上一次出现位置的下一个位置。然后更新这个字符的位置用当前窗口长度更新答案。def length_of_longest_substring(s: str) - int: last_pos {} left 0 max_len 0 for right, ch in enumerate(s): if ch in last_pos and last_pos[ch] left: left last_pos[ch] 1 last_pos[ch] right max_len max(max_len, right - left 1) return max_len这里的last_pos[ch] left这个条件很容易漏掉。为什么不直接判断ch in last_pos因为某个字符可能在窗口外出现过也就是说它上一次出现的位置已经不在当前左指针覆盖的范围内了此时不应该强行左移指针。比如abba这个字符串遍历到第二个a时字典里a的记录是0但此时左指针已经移动到2了last_pos[a] left不成立所以不需要移动左指针。这个细节我当年第一次写的时候就忽略了导致结果偏大。滑动窗口类问题在笔试里出现频率极高变体包括“含有至多k个不同字符的最长子串”“最小覆盖子串”等。理解透这道题后面做变体时思路会很顺因为核心都是“移动右指针扩张条件不满足再收缩左指针”。2.3 第三题动态规划的状态设计与转移方程推导第三题是经典的最长上升子序列问题。题目给一个无序整数数组要求返回严格递增的最长子序列长度。严格递增意味着相等元素不能算作上升序列比如[2, 2]的最长上升子序列长度是1而不是2。动态规划的第一个步骤是定义状态。我习惯用dp[i]表示以第i个元素结尾的最长上升子序列长度这个定义的好处是转移方程写起来直观当遍历到第i个元素时我往前看所有j i只要nums[j] nums[i]就可以把nums[i]接到以nums[j]结尾的序列后面所以dp[i] max(dp[j] 1)。初始化时每个元素自身构成一个长度为1的序列所以dp数组全部初始化为1。def length_of_lis(nums): n len(nums) if not nums: return 0 dp [1] * n result 1 for i in range(1, n): for j in range(i): if nums[j] nums[i]: dp[i] max(dp[i], dp[j] 1) result max(result, dp[i]) return result这个版本的复杂度是O(n²)n在一千左右时性能没问题如果n到了五位数就得换用贪心加二分法优化到O(n log n)。我当时在牛客上提交时用的是O(n²)版本测试数据规模不大直接通过了。但后面面试官追问能否优化我现场又推导了二分版本现在把两种写法都掌握才是稳妥的。动态规划的难点不是背模板而是搞清楚“状态定义”和“转移方程为什么成立”。以这道题的dp[i]定义为例它的巧妙之处在于强制要求序列以第i个元素结尾这样转移时只需要关注前一个元素是谁不用关心整个序列的具体形态。很多DP题的状态设计都是这个思路“约束结尾条件使转移可计算。”2.4 第四题贪心与排序压轴题的思维升级第四题是典型的会议安排类贪心问题给定一系列会议的开始时间和结束时间要求计算最多能参加多少个会议两个会议时间不能重叠。这种题的经典解法是按照结束时间从早到晚排序然后贪心地选择第一个结束最早的会议之后每次选择“开始时间不早于当前已选会议结束时间”的会议中结束最早的。def max_meetings(meetings): meetings.sort(keylambda x: x[1]) count 0 last_end -1 for start, end in meetings: if start last_end: count 1 last_end end return count为什么按结束时间排序关键在于结束时间越早越能留出更多空余时间去参加后面的会议。如果按开始时间排序可能选到一个开始最早但持续时间巨长的会议反而浪费了大部分时间。这种“先做某件事留出最大余量”的结构在贪心题里很常见。关于相等时间的边界题目如果允许“结束时间等于下一场开始时间”则可连着参加我的代码里用的是所以这种情况会算作不冲突。如果题目要求严格大于把判断改成即可。这个细节直接决定提交结果务必看清题目的时间边界描述。我当年就因为在和之间纠结了很久最后翻了题干才发现题目确实写了“结束时下一个会议可以立即开始”。3. 实战代码复现与常见实现细节3.1 环境准备与代码调试的基础配置在牛客上做题时代码提交的格式要求是“填写核心函数”不需要自己写文件读取和标准输出平台会自动拼接调用逻辑。所以平时练习时就要养成“只写函数体”的习惯不要花太多时间在I/O上重点是核心算法的完整度和正确性。本地调试时我喜欢用一个简单的测试框架把多个测试用例放在一个列表里循环调用函数并打印结果。这样比一次次改输入参数再运行方便得多。对于2019三模这套题我建议你也把样例输入保存成测试用例然后跑一遍确认输出符合预期再补充几个自定义的边界用例空字符串、空数组、单元素数组全相同字符的字符串已排序的数组和完全逆序的数组会议时间全不重叠、全重叠等极端情况这些边界用例能帮你把隐藏问题提前暴露出来。很多同学自定义用例只测“正常情况”觉得样例过了就万事大吉实际提交时挂掉的往往就是边界条件。3.2 Python实现四道题目的完整代码方案把上面四个题型的代码整合到一起就是一套完整的2019牛客三模Python参考实现。我仔细核对过这几个版本的变量命名和逻辑边界直接复制到本地跑一遍就能看到结果。# 第一题括号匹配 def is_valid_brackets(s: str) - bool: stack [] pairs {): (, ]: [, }: {} for ch in s: if ch in ([{: stack.append(ch) else: if not stack or stack[-1] ! pairs.get(ch): return False stack.pop() return len(stack) 0 # 第二题最长无重复子串 def longest_unique_substring(s: str) - int: last_pos {} left 0 max_len 0 for right, ch in enumerate(s): if ch in last_pos and last_pos[ch] left: left last_pos[ch] 1 last_pos[ch] right max_len max(max_len, right - left 1) return max_len # 第三题最长上升子序列 def lis_length(nums) - int: n len(nums) if n 0: return 0 dp [1] * n result 1 for i in range(1, n): for j in range(i): if nums[j] nums[i]: dp[i] max(dp[i], dp[j] 1) result max(result, dp[i]) return result # 第四题最多会议数 def max_meetings(meetings) - int: meetings.sort(keylambda x: x[1]) count 0 last_end -1 for start, end in meetings: if start last_end: count 1 last_end end return count这几个函数都用纯Python书写没有依赖第三方库除了第四题用到了lambda排序其他都是最基础的语法。哪怕是刚学Python不久的同学也应该能看懂。刷题的时候我不建议用第三方库里的现成算法函数因为笔试环境不一定允许而且自己手写一遍能加深对数据结构底层逻辑的理解。3.3 时间复杂度和空间复杂度的评估思路笔试里做完题往往还要在面试环节面试官复杂度分析是绕不开的。第一题每个字符入栈出栈各一次时间复杂度O(n)空间最坏情况O(n)因为字符串可能全是左括号。第二题双指针各移动一次时间复杂度O(n)空间上用字典存字符位置长度不超过字符集大小通常可以视为O(字符集大小)。第三题双重循环的O(n²)复杂度是重点优化成贪心二分的版本时间复杂度是O(n log n)这题的优化思路值得单独写出来维护一个数组tails其中tails[i]表示长度为i1的上升子序列的最小末尾元素遍历每个数字在tails中二分查找第一个大于等于当前数字的位置替换掉如果当前数字比tails所有元素都大就追加到末尾这个思路理解起来比O(n²)复杂一些但面试时能讲清楚会很加分。我个人的学习路径是先把O(n²)版本写熟完全理解状态转移再去啃贪心二分的优化版本这样脑子里会有一条清晰的演进路线而不是死记硬背优化代码。第四题的贪心策略排序耗时O(m log m)m是会议数量循环遍历是O(m)整体以排序为主。空间上是O(m)还是O(1)取决于排序是否使用了额外空间Python内置的TimSort是O(n)空间。4. 从踩坑到避坑牛客提交核心经验总结4.1 我在这套题上反复栽过的三个细节错误第一个错误是第二题滑动窗口里漏判last_pos[ch] left。当时我写的判断条件是if ch in last_pos:结果遇到abba这种字符串时左指针已经移动了但字典里旧字符的索引还停留在之前的位置导致窗口左边界被错误地拉回到了更小的位置最终最长子串长度被算大了。第二个错误是第一题“括号匹配里我只处理了右括号导致栈为空的情况没处理左括号有多余字符的情况。输入(()时栈里还剩一个左括号但我已经提前返回True了。后来在最后加了一行return len(stack) 0才把所有情况都覆盖到。第三个错误是第四题读题不仔细没注意到会议结束时间和下一场开始时间的关系设定。我一开始用的是if start last_end结果如果会议A是[1, 4]会议B是[4, 6]按我的写法B就参加不了但题目实际上说的是可以连着参加。改回才通过。这三个错误都不是算法思路问题全是细节处理问题但笔试就是这样思路对了但细节错了一样拿不到分。4.2 牛客平台的评测机制和应对策略牛客的编程题平台会跑多组隐藏测试用例而不是只跑样例。所以“能跑通样例”和“能通过全部测试”之间往往有很大距离。我的习惯是样例通过之后结合题目约束条件主动构造若干边界用例来验证比如字符串长度为1或2的极短场景数组元素全部相同或全部成升序所有会议时间一致的最极端冲突场景实际提交后如果显示部分用例通过平台通常会给出失败的是“运行超时”还是“答案错误”。运行超时说明算法复杂度太高需要换优化算法答案错误则需要进一步检查逻辑短路或者边界处理。这个信息很关键能帮你快速缩小问题范围。笔试时提交次数通常有限制不能无限试错所以把时间花在“充分本地测试”上远比“反复盲交”更有效率。4.3 把这些经验迁移到其他笔试平台2019牛客三模的这套题本质上代表了一类笔试题目风格题干简洁不设陷阱算法思路经典主要考察的是“基本功是否扎实”。LeetCode的题目风格接近这个方向但部分题目难度明显更高。而在一些传统公司的自研平台上有时候会比较繁琐需要解析复杂的输入格式比如先读一个整数n再读n行数据这时候字符串解析能力也很重要。我给读这篇文章的读者的建议是刷完一套模考题后不要急着做下一套先把错题和超时的题目整理成一份自己的题解笔记记录思路、代码和踩坑点。如果每套题都能沉淀出这样的总结十套卷子之后你的知识框架会非常系统远比盲目刷三百道LeetCode有针对性。因为模考的题型分布和真实笔试最接近它能帮你找到知识体系的薄弱环节。5. 拓展思考这套题背后的出题逻辑与复习策略5.1 从四道题反推笔试命题人的考察意图2019牛客三模的命题人安排这四道题目的很清晰第一题考察编码的基本功和细心程度栈操作不复杂但条件分支多容易在边界上犯错第二题考察对双指针技巧的掌握这几乎是所有笔试必考的高频题型第三题考察基础动态规划能力状态定义和转移方程是最核心的算法思维训练第四题考察贪心策略和排序技巧是面试中深挖问题的高发区。如果你站在面试官的角度思考就会发现这些题不只是为了筛人更是为了在后续面试中引出更深层次的讨论。比如你写出了第三题O(n²)的解法面试官大概率会追问“能不能优化”这就是考察你是否具备算法优化的意识和能力。所以我一直强调刷题不只是为了“通过代码提交”还要能做“口头代码讲解”把每一行代码背后的逻辑讲清楚。5.2 如何利用这套题制定自己的刷题计划如果你现在距离笔试还有一个月到两个月的时间我的建议是使用“三遍刷题法”来充分利用模考套题。第一遍按考试状态限时完成整套题模拟真实考场的节奏第二遍把做错的题和卡壳超过二十分钟的题整理出来逐题精读解析重新手写实现第三遍隔一周之后再回来重刷同样的题检验自己是否真正理解了解题思路。这套方法看起来很费时间但实际上比“广撒网”式刷题效率高很多。因为模考套题的数量有限每套都承载着大量考点通过反复精做吃透其中考点的几率比到处找难度不一的题库乱刷要高得多。我当时就是靠着把近两三年的牛客模考卷都做了一遍每套题至少刷两遍最后在秋招笔试里的通过率明显提升了。5.3 结合Python基础能力训练的建议有读者看到“python2025.3一级编程题”这样的热词也来问我Python基础阶段怎么样准备才行。我的建议是算法学习与Python语法学习是相辅相成的不必先花三个月把语法背完再开始刷题。边刷题边查语法遇到不会的内置函数直接搜索效率反而更高。比如上面四道题里用到的enumerate、sort(key...)、max、dict.get这些用法都是Python刷题时最高频的语法点从一开始接触就能顺手学会。实际上把一套题的题解用Python写下来本身就是在训练Python基础编程能力。等你把几十道题吃透那些“列表推导式”“sorted排序参数”“字典取值技巧”几乎都会在题目中自然遇到并掌握比单独找一本语法书从头背到尾扎实得多。6. 我个人在反复刷这套题之后的实操感受说点题外话。2019牛客三模这套题我在当年秋招季前前后后刷了三遍。第一遍做的时候最长上升子序列那道题我只写出了O(n²)版本会议安排那道题因为“等于”边界写错了还被牛客的测试用例查了出来。等到第二遍刷的时候我已经可以不看任何参考把四道题一次性全部通过。第三遍刷其实是用它来练面试表达模拟自己对着面试官讲解解题思路的节奏。所以我想说说给准备笔试的朋友一句实在话一套含金量高的模考题值得你反复刷直到闭上眼都能把核心思路默写出来。不要觉得一道题做过就完事了遗忘曲线对算法题一样有效。间隔一周左右重新做一遍你就能明显感觉到哪些知识是真的懂了哪些只是当时背下来了。这个过程虽然枯燥但效果立竿见影。2025年这个时间点再回头来看2019年的模考题你会发现虽然年份变了但笔试面试中高频考点的变化并不大栈、字符串、动态规划、贪心依旧是每轮招聘季的“常驻嘉宾”。把基础题型的解法内化成肌肉记忆再去面对任何新题都会从容得多。这也是我写这篇文章的核心原因希望你看完能对这套题有一个系统的认知并带着清晰的思路去动手敲代码。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进