2019牛客三模编程题实战解析:从模拟题到递推DP的笔试核心技巧
2026/9/1 22:27:06 网站建设 项目流程

作为一个当年在牛客网上反复刷模拟题、秋招前被三模虐到怀疑人生又硬啃下来的过来人,看到这个标题我还是很有感触的。2019牛客三模的编程题集合,放在当时是很多人秋招前的一次重要演练,现在回头看,里面好几道题考察的思维点和边界处理方式,放到今天的笔试里依然很经典,尤其是那类“看起来简单、一写就错”的模拟题和带点动态规划味道的递推题。这篇文章我把当时做题的完整思路、代码实现和踩过的坑都整理出来,给正在准备笔试的朋友一个参考。

这个题目集合适合谁看?主要还是准备校园招聘笔试、想熟悉在线评测系统(OJ)判题规则的人,不管你是后端、客户端还是测开方向,编程题都是躲不掉的一关。文章不会只贴答案,而是把每道题的思考过程、为什么这样做、以及常见的错误点讲清楚,这样你遇到变体题也不慌。

1. 整体设计与思路拆解

1.1 19年三模编程题的考察倾向

我印象很深的点是,2019牛客三模的编程题整体难度不算特别大,但陷阱非常多。它不像一些竞赛题那样上来就让你写平衡树或者网络流,而是更贴近“实际工作中的编码能力”和“面试手撕代码的基本功”。

从考点分布上看,核心集中在几个方面:

  • 字符串处理与边界判断,尤其是输入可能带空格、空行、大小写混合的情况。
  • 模拟类题目,题目描述很长,本质就是让你按规则一步步实现,需要提炼核心逻辑。
  • 基础的排序和查找,但要求你考虑稳定性或者复杂度,不能无脑调API完事。
  • 递推和简单动态规划,往往伪装成“数学题”或者“找规律题”,实际是状态转移。
  • 数据范围陷阱,比如很大整数要用long long或Python直接支持大整数,否则C++选手会栽。

这个设计思路其实很聪明。秋招笔试不是选拔竞赛选手,而是在有限时间内考察候选人能不能写出清晰、正确、健壮的代码。所以那些“看起来简单,但容易考虑不全”的题,反而最能拉开差距。

1.2 为什么说这是一套练手价值很高的题目集

很多人会觉得考完试题目就没用了,但我不这么看。这套题的价值在于它模拟了真实笔试中最容易出现的“审题偏差”。

举个例子,有一道题要求对字符串做某种替换,描述里写着“按照字典序最小的方式输出”。很多人的第一反应是直接排序,但仔细分析之后会发现,如果你直接把整个字符串排序,会破坏原有字符的相对位置关系,而题目的隐含要求可能是“在保持原顺序的前提下,让某一部分字典序最小”。这种差异化理解,正是三模想考验你的地方。

另外,这套题还很喜欢在“输入输出格式”上做文章。比如有的题目输出答案时要换行,有的要求每个结果之间用空格分隔,末尾不能有空格;有的数据是多组输入,读到某个标志才结束。这些细节在本地IDE里你根本不会注意,但上了OJ就是一次次Wrong Answer。我当时就因为这个原因,交了七次才过了一道题。所以说,平时用牛客这种在线评测系统刷题,能帮你提前适应这种严苛的判题环境,这是只在本地上跑跑用例完全比不了的。

2. 核心细节解析与实操要点

2.1 第一类典型题:带规则的字符串变换

这类题在笔试里出现频率极高,核心是考察对字符串下标和边界条件的把控。我当时遇到的一道题大致是这样:给定一个只包含小写字母的字符串,要求把所有连续相同的字符压缩成“字符+出现次数”的形式,如果压缩后的字符串长度没有变短,则输出原字符串,否则输出压缩结果。看起来无非就是遍历一遍,但下面几个点很容易犯错。

第一个坑是次数可能超过一位数。比如“aaaaaaaaaa”(10个a),压缩结果是“a10”,长度是3,确实是变短了,但如果你的代码里只是用count + '0'来拼接,超过9就全乱了。正确做法是使用整型转字符串的函数,C++里可以用to_string,Java里是String.valueOf,Python里直接用str()就行。

第二个坑是“长度没有变短则输出原字符串”的判断时机。你不要每处理一个字符就判断一次,那样逻辑会很乱。正确做法是先完整生成压缩串,最后判断压缩串和原串的长度大小,再决定输出哪个。这里有一个小技巧,如果压缩串中途长度已经超过原串,理论上可以提前终止,但对笔试而言没必要做这种优化,反而容易写错。

第三个坑是末尾字符的收尾。我见过很多人写循环时,只在当前字符和下一个字符不同的时候处理前一个字符的计数,结果循环结束后忘记处理最后一组字符,导致漏掉了末尾一串字母。这个错误太经典了,几乎所有新手都会踩一次。我的习惯是在循环里先比较ii+1,不相等就结算之前的,循环结束后再把最后一段补上。

2.2 第二类典型题:模拟题中“状态机”思维的重要性

三模里还有一道排队模拟的题,具体规则我记不太清了,大概是说有一堆任务按照不同优先级到达,每次执行一个时间片,然后判断某个时间点的任务状态。这类题看起来简单,但如果直接用循环模拟每个时间片,一旦数据范围大了就会超时,而且代码冗余。

我当时用的是“状态机”的思路来简化。先分析一个任务在整个过程中可能处于哪些状态:等待、执行、完成。然后根据时间推进,只关心在关键时间点上的状态变化,而不是一个时间片一个时间片地推进。比如任务在时间t到达,执行时长为p,如果当前任务队列为空,那么任务立刻执行;否则需要等待前面所有任务完成。这样就能推导出每个任务的开始时间和结束时间,而不用真的去模拟每一秒。

这里同样有个大坑:如果多个任务到达时间相同,优先级怎么处理?题目一定会定义清楚,比如按编号先后,或者按优先级。读题时务必圈出来,因为排序规则会直接影响结果。我建议把任务封装成一个结构体,包含到达时间、执行时长、编号、优先级,然后用一个优先队列来维护。在C++里,priority_queue默认是大顶堆,如果你想按优先级高的先出队,需要自定义比较函数;而Java的PriorityQueue则是最小堆,使用方式略有不同。Python里可以用heapq,存进元组的时候注意排序字段的顺序。这个技巧我至今仍在用。

2.3 第三类典型题:不动点与循环节思想

还有一道题让我印象很深,题目大意是:给定一个正整数n,如果它是偶数就把n除以2,如果是奇数就把n乘以3再加1,重复操作,问经过多少次可以变成1。这就是著名的考拉兹猜想(3n+1问题)。题目本身不要求证明,只是实现,看起来简单到不行。

但这道题真正的考点是“数据溢出”。当n较大时,在变成1的过程中,中间值可能会先变得非常大。比如n=27,中间最高值能达到9232,这还算温和;如果是更大的数,C++的int绝对会溢出。所以这类题只要涉及乘法加一,一定要用long long,甚至unsigned long long。如果用Python,可以暂时高枕无忧,因为Python的整数是任意精度的。

另一个考点是“循环检测”。虽然考拉兹猜想至今没有被证明,但如果在题目条件里加入了一个特定模数,或者让你判断某个数会不会重复出现,那就可以用哈希集合记录访问过的数,一旦重复就认为进入循环。这是一种通用套路,在模拟“走迷宫死循环”类题目中常见。我当时在做这道题时就额外实现了一个set来记录,虽然题目不一定需要,但作为一种防御性编程,对思考很有帮助。

2.4 第四类典型题:看似数学题实则DP的数列问题

还有一类题很迷惑人,题目会给你一个递推公式,比如f(n) = f(n-1) + f(n-2) - f(n-3),然后让你求第n项。有些人第一反应是直接递归,结果n稍微大一点就栈溢出或者超时。更有些人尝试去找数学通项公式,但大多数情况下并没有那么简单的闭式解。

这道题的正确打开方式是动态规划,用一个数组或者几个变量滚动保存前几项的结果。为什么说是DP而不是“递推”?因为递推关系和DP状态转移本质上是同一个东西,但DP强调的是“无后效性”和“重叠子问题”。在这里,f(n)只依赖于前三项,所以你不需要保存所有历史的f值,只需要维护最近三个状态,每次迭代滚动更新就好了。

具体到代码,可以定义三个变量a, b, c分别代表f(n-3), f(n-2), f(n-1),然后循环计算d = c + b - a,更新完再整体往前移动一位。这个做法的空间复杂度是O(1),时间复杂度是O(n),对于笔试场景是标准答案。我见过很多人虽然能写出递归,但无法优化,遇到n=10^7直接歇菜。所以这题考的不只是公式,而是你能不能把递归改成迭代的滚动数组。

3. 实操过程与核心环节实现

3.1 环境准备与输入输出套路

我强烈建议在正式刷题之前,先搞定“输入输出模板”。这不是浪费时间,因为在笔试中,很多人不是不会写算法,而是卡在“怎么读入一整行包含空格的字符串”“怎么处理多组输入直到EOF”这种事情上。

以Python为例,常见模板就是:

import sys def solve(): data = sys.stdin.read().strip().split() # 处理单个数字 n = int(data[0]) # 处理多组输入直到EOF tokens = sys.stdin.read().split() i = 0 while i < len(tokens): a = int(tokens[i]); b = int(tokens[i+1]) # 做点什么 i += 2 if __name__ == "__main__": solve()

不要用input()一行行读,当数据量大的时候,sys.stdin.read()一次性读入再切分是最稳的。C++则建议关闭同步流,在main函数开头写一句ios::sync_with_stdio(false);,再配合cin.tie(nullptr);,否则容易超时。这个习惯我从三模一直保持到现在,很多ACM选手也是这么写的。

另外,注意输出格式。如果题目说每行输出一个结果,就老老实实print(ans);如果要求用空格分隔,可以用print(" ".join(map(str, ans_list))),这样自动不会有多余空格。我在三模时因为多打了一个空格被判Presentation Error,虽然判题系统没有算错,但这种无谓的罚时很不值。

3.2 字符串压缩题的参考实现与细节

为了直接能用,我给出一个经过反复测试的Python版本:

def compress(s: str) -> str: if not s: return "" res = [] cnt = 1 for i in range(1, len(s)): if s[i] == s[i-1]: cnt += 1 else: res.append(s[i-1]) res.append(str(cnt)) cnt = 1 res.append(s[-1]) res.append(str(cnt)) compressed = "".join(res) return s if len(compressed) >= len(s) else compressed

这段代码的重点在于循环里比较ii-1,而不是ii+1,这样就不太容易漏掉最后一组。最后用len(compressed) >= len(s)作为是否保留原串的条件,符合题目的“没有变短则输出原串”。如果要求相等时输出原串,那这个判断就是对的;如果要求相等时输出压缩串,改成>即可。这个细节一定要看清题目。

我在测试时发现很多人会忽略一个问题:如果原串里面已经有数字,压缩后的表示就可能产生歧义。当然题目通常限定只包含小写字母,但如果题目没有明确说明,那么压缩方案本身就不严谨。所以在拿到题时,先确认输入约束条件,这会省下一堆麻烦。

3.3 任务模拟问题的参考实现与分析

我给一个简化版的思路示例,重点在于数据结构和事件处理逻辑。假设我们有任务列表tasks[(arrive, duration, priority, id)],需要输出每个任务的完成时间。

import heapq def process_tasks(tasks): tasks.sort() # 按到达时间排序 heap = [] idx = 0 current_time = 0 ans = {} n = len(tasks) while idx < n or heap: if not heap and idx < n and current_time < tasks[idx][0]: current_time = tasks[idx][0] while idx < n and tasks[idx][0] <= current_time: arrive, duration, priority, tid = tasks[idx] heapq.heappush(heap, (-priority, arrive, tid, duration)) idx += 1 if heap: neg_pri, arrive, tid, duration = heapq.heappop(heap) current_time += duration ans[tid] = current_time return ans

这段代码的思路是:先把任务按到达时间排序,维护一个小顶堆,堆内以负优先级作为键,这样优先级高的任务会先被弹出;current_time表示当前系统时间,当堆为空且下一个任务还没到,就直接把时间跳到下一个任务的到达时间,避免无意义的空转。整个过程是O(n log n)的,足够应对大多数笔试数据范围。

需要注意的一点是,如果两个任务优先级相同,题目往往会要求先到达的先执行,所以堆元素里的第二个键可以用arrive,如果还相同再用id,保证同优先级时按照到达顺序或输入序号执行。你还需要自己确认,如果任务在执行过程中来了更高优先级的任务,是允许抢占还是非抢占。三模里那道题应该是非抢占的,也就是一旦开始执行,就不会被新任务打断。如果题目要求抢占式调度,那么代码要改成每次新任务到达时就重新比较剩余时间,逻辑会复杂一些。

3.4 考拉兹类模拟题的实现与细节

这道题的实现思路非常直白,但要注意数据类型和步数上限。下面给出C++和Python两种版本。

C++版:

#include <iostream> using namespace std; int main() { long long n; cin >> n; int steps = 0; while (n != 1) { if (n % 2 == 0) n /= 2; else n = n * 3 + 1; steps++; if (steps > 1000000) break; // 防止意外死循环 } cout << steps << endl; return 0; }

Python版:

def collatz_steps(n: int) -> int: steps = 0 while n != 1: if n % 2 == 0: n //= 2 else: n = n * 3 + 1 steps += 1 # 预防卡死,可加一个合理的上限 if steps > 10**6: return -1 return steps

注意Python中偶数操作要使用整除//,如果写成/会变成浮点数,不但慢还会引起精度问题。C++里则要注意乘法溢出,所以n字段必须定义为long long。如果你看到题目给的初始n特别接近10^9,那么中间值可能会接近10^9乘以3甚至更大,32位int绝对不够。我当年就见过有人用int,结果本地测试小数据时全对,一提交就WA,还找不到原因。所以类型选择是这类题的第一道坎。

很多题目还会继续追问:“输出过程中出现过的最大值是多少”,那就在循环里维护max_val变量,每次更新max_val = max(max_val, n)即可。把它们合在一起,实现起来并不难,关键是想清楚是否有必要记录历史值,这取决于题目问的是什么。

3.5 滚动数组DP的参考实现

假如题目递推式是f(n) = f(n-1) + f(n-2) - f(n-3),初始给出f(0), f(1), f(2),求f(n)。那么代码可以这样写:

def get_f(n: int, f0: int, f1: int, f2: int) -> int: if n == 0: return f0 if n == 1: return f1 if n == 2: return f2 a, b, c = f0, f1, f2 for _ in range(3, n + 1): d = c + b - a a, b, c = b, c, d return c

这里a, b, c对应的是f(i-3), f(i-2), f(i-1),每算出一个d = f(i),就把三数整体前移。这个做法的好处是无论n多大,只要O(n)时间可承受,空间永远是常数。

不过需要注意,递推式中涉及减法,结果可能为负,也可能增长很快。如果题目要求取模,比如“对10^9+7取模”,那么每次得出d之后就要立刻取模,并且注意在c + b - a时,Python的负数取模结果仍然是正数,直接用(c + b - a) % MOD即可;而C++的负数取模会得到负值,需要写成((c + b - a) % MOD + MOD) % MOD来保证结果正确。这是很多人容易翻车的地方。

如果n特别大,比如10^18,那就不能再用O(n)的循环了,这时候要用矩阵快速幂加速递推。三模的题目大概率不会考到这种程度,但作为延伸,如果你以后遇到这类题目,就得会构造转移矩阵。递推式对应的矩阵形式为:

| f(n) | | 1 1 -1 | | f(n-1) | | f(n-1) | = | 1 0 0 | * | f(n-2) | | f(n-2) | | 0 1 0 | | f(n-3) |

用矩阵快速幂可以在O(log n)时间内求出第n项。我个人觉得,就算三模不考,你也值得掌握,因为很多公司的笔试会把简单递推藏在高数据范围后面,专门筛选那些只会递归的人。

4. 常见问题与排查技巧实录

4.1 多次Wrong Answer,到底错在哪

我在三模那会儿最崩溃的不是题不会做,而是明明本地测试都对,一交上去就WA。后来我总结了一套排查流程,非常适合比赛和笔试中使用。

第一,检查数据范围。这是最容易被忽视的。题目里说n小于等于10^9,那么你在计算答案的过程中会不会用到乘法?如果用int存,中间结果可能已经溢出,最终答案自然不对。遇到这种题,一律用long long,Python选手则不用太担心,但要注意浮点数与整数的区别。

第二,检查多组输入。题目可能没有明说“多组测试数据”,但在线评测时往往会用多个用例同时验证。如果你的代码只处理了一个用例,第二个用例开始就会错。所以养成习惯:如果输入模板是按行读取,尽量在主循环里用while处理到EOF,除非题目明确说只有一个用例。

第三,检查输出格式。输出多余的空格或换行,通常会被判为Presentation Error。虽然它不计为WA,但会耗费你宝贵的试错次数。还有一种情况是要求“每个结果后跟一个换行”,而你用了空格分隔,也会WA。

第四,检查特殊边界。字符串为空、n等于0、数组长度为1、输入中有前导零、数据包含负值等等。在提交前,逐一遍历这些边界,手动构造最小的输入来本地跑一遍,往往能立刻发现问题。

我给一个通用的“自测模板”习惯:写代码之前先设计几个用例:

  • 普通情况:比如在压缩字符串中用aaabbb,期望输出a3b3
  • 边界情况:字符串只有一个字符a,压缩后是a1,长度等于或大于原串,应输出原串a
  • 极端情况:全相同且长度很大,比如aaaaa...,压缩后应该明显更短。
  • 空输入:如果题目没说不会给空串,那if not s的分支不能省。

这些用例写完,再提交,通过率会高很多。

4.2 数组越界和空指针的防范技巧

在线编程题中,数组越界是另一种高发错误。尤其是C/C++,数组越界通常不会当场崩溃,而是会覆盖相邻内存,造成难以捉摸的错误。我见过有人写for (int i = 0; i <= n; i++)访问a[n],而数组定义长度只有n,这种问题很难发现。解决方法是:在写循环时,反复确认上下界。比如访问a[i]a[i+1]时,循环条件一定要是i+1 < len,而不是i < len。在Java里,一旦越界会抛出ArrayIndexOutOfBoundsException,相对容易定位,但Python的列表越界会抛IndexError,也很好发现。真正危险的是C++这种不强制检查的语言。

空指针问题在Java中较常见,比如使用一个对象前没有判断是否为null。虽然在笔试的纯算法题中不常遇到,但如果题目要求设计数据结构,比如链表操作,那就要注意边界节点的判空。我的建议是,在debug时打印关键位置的变量值,不要只靠脑子想。在线OJ上可以先用小样例暴力输出中间结果,肉眼确认没问题后再删掉调试代码。

4.3 超时的常见原因与优化方向

如果你碰到的是TLE(Time Limit Exceeded),那问题往往出在算法复杂度上。比如一道题如果n等于10^5,O(n^2)的算法就会超时,必须把复杂度降到O(n log n)或O(n)。这种情况在三模里也很常见,尤其是模拟题,如果你真的一个时间点一个时间点去推,数据大一点就GG。我前面提到的用优先队列模拟任务调度,就是典型的从O(total_time)优化到O(n log n)的思路。

还有一种超时是语言本身的输入输出太慢。Python的input()print()在大量数据时不如sys.stdin.read()sys.stdout.write()。C++的cin/cout如果不同步,也会比scanf/printf慢很多。虽然现在的OJ大多放宽了时限,但保险起见,还是用更快的方式为好。我之前在三模的某道题里就用sys.stdin.buffer.read(),比sys.stdin.read()还要快一些,数据量很大时差距更明显。

4.4 从WA到AC的调试实战记录

我说一个真实的例子。当时有一道题要求统计一个整数数组中有多少对元素的和等于目标值k。我第一版写的O(n^2)双重循环,提交后TLE了。然后我想到用哈希表,遍历数组,对于每个元素num,看k-num在不在哈希表里。这个思路本身没问题,但我第一次写时,是先全部塞进哈希表再遍历,这会导致同一对元素被统计两次,而且存在重复值的数组还容易把三元组也算进去。

我当时的修复方案是“边遍历边统计”,也就是先查k-num的计数,再把当前num加入哈希表。这样就保证了每一对元素只会统计一次,而且不需要考虑下标先后问题。修改之后,又出现了一个边界:如果num等于k-num,比如k=6,数组里只有一个3,那么理论上没有配对,但如果先插入再查询,或者先查询后插入但不做计数控制,可能就会把自己也算进去。这个问题的本质是“同一元素不能使用两次”。正确做法是:查询的是之前已经遍历过的元素,而不是当前的元素,这样才能避免使用同一个元素两次。

这个小案例说明,很多题目的正确解法并不是难想,而是细节容易出错。当你WA到怀疑人生时,不要急着改代码,先回过来把题目的约束再读三遍,尤其是“是否可以重复使用元素”“是否要求下标不同”这些限定词,往往就是关键。

4.5 考场上的时间分配与提交策略

还有一点经验想分享,虽然不算技术,但很实用。三模的题目数量通常是四道左右,时间有限,如果一道题卡了半小时还没思路,果断跳过,先做后面的题。笔试的评分很多时候是按通过的测试用例比例给分,所以即使你只过了部分用例,也比交白卷强。我当时的策略是:

  • 先花5分钟把所有题都看一遍,标注每道题的难度和可能用到的算法。
  • 从最容易拿分的题目开始做,保证至少有一道题是完整AC的。
  • 遇到难题,如果想到了一个O(n^2)的解法,数据范围不大就先写,能拿部分分就拿部分分;如果数据范围大,那就先写一个暴力版本保底,再思考优化。
  • 提交前,至少留5分钟检查输入输出格式和边界用例。

4.6 牛客模考成绩不理想,还有救吗

最后想再说一句,三模只是模拟,成绩不理想不代表秋招就没救。我当年三模只做了两道半,当时特别沮丧,但后来总结发现,那两道半其实帮我暴露了很多问题:读题不仔细、边界处理差、数据结构不熟练。于是我把每道错题都整理成笔记,写上错误原因和正确思路,之后又去刷了牛客的历年真题和类似题型,大概两周后,水平就有了明显提升。所以,如果你也正在被模考虐,恭喜你,这是提前暴露问题的机会,比在正式笔试中踩坑要幸运得多。

我的建议是准备一个错题本,不用抄题,只要记录知识盲区和坑点:比如“以后遇到XX条件,记得用long long”“输出要求严格换行”“模拟题先看是否支持抢占”等等。到正式笔试前翻一遍,效果非常好。我个人现在碰到复杂题目,还是会下意识使用三模时总结的那套结构化思考方式:场景模拟先抽象状态,字符串处理先想边界,递推关系先想滚动数组,排序问题先想稳定性和复杂度。这套方法论,比记住某道题的答案有价值得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询