牛客模考五模编程题复盘:高频算法考点与笔试技巧
2026/8/31 6:12:20 网站建设 项目流程

2023年秋招,我的刷题计划里一直留着牛客的模考卷。原因很简单:正式笔试前,需要有一套和真实环境一样有输入输出、有AC率、有排行榜的题目来模拟一遍。牛客模考(五模)的编程题集合是我印象比较深刻的一套,四道编程题覆盖了字符串处理、贪心区间、搜索、动态规划这些笔试常客,难度梯度设置得也比较像大厂正式笔试。对于正在准备春招、暑期实习笔试或者刚开始刷题的人,这套题的价值不只是“做了几道题”,而是能帮你快速定位自己在哪些考点上还处于“似懂非懂”的状态。

本文将基于这套题里最有代表性的几道题,还原我的解题思路、现场踩过的坑,以及考后复盘时的总结。代码直接用Python写,因为牛客笔试环境里用Python的同学越来越多,而且这套题用Python实现起来足够清晰。

1. 2023五模题集在笔试复习中的定位

1.1 模考卷的构成与难度分布

牛客的模考一般是按照主流互联网公司笔试节奏设计的,常规构成是选择题加编程题,编程题通常四道,难度从签到题到压轴题逐渐递增。五模这套编程题集合给我的整体感觉是:前两道属于“基本功”,第三道开始出现搜索和状态设计,最后一道如果不熟悉动态规划,很容易写出一份超时或者答案错误的代码。

我当时整理了这套题的考点分布,大致可以画成这样的表格:

题号核心考点推荐解法难度
第一题字符串处理一次遍历模拟简单
第二题区间调度贪心 + 排序中等
第三题二维网格连通块DFS / BFS中等
第四题完全背包 / 最少硬币一维动态规划较难

这个分布其实代表了笔试编程题的典型思维层次:先看你会不会处理基础输入和字符串切片,再看你有没有“排序后贪心”的意识,然后考你能不能把图论模型套到二维网格上,最后用动态规划筛掉只会暴力枚举的人。

1.2 为什么值得专门写一遍

很多人刷题习惯在 LeetCode 上按题号刷,但牛客这类 ACM 风格平台对笔试的还原度更高。LeetCode 的核心函数模板帮你封装好了输入输出,而牛客要求自己处理标准输入流,一个多余的换行、一次错误的split都有可能让你交出一份“本地能跑,提交 CE/WA”的代码。

五模编程题最大的价值,就是让你提前熟悉笔试系统的脾气。它会告诉你:不是思路对了就能 AC,输入读取、边界值、循环退出条件,任何一个环节出问题都是零分。我在正式秋招前把五模完整做了一遍,考场上遇到类似题时,心态确实稳了很多。

2. 高频考点的底层逻辑

2.1 字符串处理:从“模拟到恶心”到“一行函数搞定”

第一题考字符串压缩,这类题在笔试里出现频率极高。它本身不难,但很容易被“复杂模拟”带偏。比如有些人看到题目就开始写循环套循环,还要考虑字符的边界情况,最后代码写得又长又容易漏。

真正高效的思路是:单次遍历,记录当前字符和出现次数,当字符变化时把结果写入列表,最后统一拼接。Python 里字符串是不可变对象,每次+=都可能产生新的对象,所以别用字符串不断拼接,而是先存到列表里再''.join()。这不是炫技,而是为了在数据量大的时候避免不必要的性能损耗。

这类题还有一个隐藏考点:读题。有时候题目要求“连续相同字符超过一次才计数”,有时候要求“全部字符都要带上次数”。我在五模时就是因为没注意题目的输出格式要求,把a3b2c1写成了a3b2c,白丢了一次提交。所以写字符串题之前,先花十秒钟看清楚输出示例。

2.2 贪心与排序:为什么排序方向错了就全盘皆输

第二题是经典的“最多能参加多少个会议”类问题。这类题的结论很明确:按结束时间升序排序,然后贪心选择结束时间最早的会议,再跳过所有与它冲突的会议,重复这个过程。

关键点是排序依据。我见过不少同学按开始时间排序,然后发现答案不对。其实可以自己构造一个反例:会议A是[0, 10],会议B是[1, 2],会议C是[3, 4]。如果按开始时间排,会先选A,但A一个会议就占满了整天;按结束时间排,会先选B再选C,能得到最优解“2个会议”。

这个例子很直白地说明了贪心策略的局部最优是否能推到全局最优。结束时间越早,给后面留下的时间越多,这是直觉,也是证明。笔试中遇到区间类题目,先判断能不能用贪心,再立刻想到排序。

2.3 搜索与动态规划:先建模再动手

第三题和第四题分别是网格连通块和最少硬币。它们看起来完全不同,但都有一个共同点:先建模,再套模板。

“岛屿数量”就是二维网格里的连通块数量问题,建模为无向图,每个为1的格子是一个节点,上下左右相邻的格子之间连边,然后统计有多少个连通分量。DFS、BFS、并查集都能做。现场最容易出问题的是忘记把访问过的格子标记为“已访问”,导致无限递归或者重复计数。

“最少硬币”是典型的完全背包变体。硬币数量无限,要凑出总金额,求最少硬币数。暴力递归会超时,需要从底向上构建DP数组:dp[i]表示凑出金额i需要的最少硬币数,初始为无穷大,dp[0]=0。状态转移是dp[i] = min(dp[i], dp[i - coin] + 1),反过来遍历每种硬币即可。写这类题时,先确定状态定义,再写转移方程,最后检查初始化。

3. 五道代表性题目的完整复盘

3.1 字符串压缩:一场关于“输出格式”的较量

题目描述:给定一个仅由小写字母组成的字符串,将其中连续出现的相同字母压缩为字母+出现次数的形式,例如aaabbc输出a3b2c1。输入一行字符串,长度不超过1000,输出压缩后的结果。

输入示例

aaabbc

输出示例

a3b2c1

解题思路:一次遍历,维护当前字符cur和计数器cnt。当遇到新字符时,把cur + str(cnt)加入结果列表,然后重置。遍历结束后再处理最后一组字符。这样可以保证时间复杂度 O(n),空间复杂度 O(n)。

参考代码

s = input().strip() if not s: print() exit() res = [] cur = s[0] cnt = 1 for ch in s[1:]: if ch == cur: cnt += 1 else: res.append(cur + str(cnt)) cur = ch cnt = 1 res.append(cur + str(cnt)) print(''.join(res))

复盘要点:我提交时犯过一个低级错误,没有判断空字符串。虽然题目说字符串非空,但我还是习惯性加了保护。另一个容易丢分的地方是“如果某个字符出现一次,输出a1还是省略成a?”这个必须严格按题目要求来,不确定时多看一眼示例。牛客的判题很严格,输出多了空格或者少了换行都可能判错。

3.2 会议安排:贪心排序证明

题目描述:给定n个会议的开始时间和结束时间,每个会议用[start, end]表示,你最多能参加多少个互不重叠的会议?会议结束时间严格大于开始时间,n <= 10^5

输入示例

4 1 2 3 4 0 6 5 7

输出示例

2

解题思路:按照结束时间从小到大排序,然后遍历。记录当前已选择的最后一个会议的结束时间last_end,如果当前会议的开始时间大于等于last_end,则选择它,并更新last_end。这个策略能在同样结束时间下选更多会,并且留下的剩余时间最多。

参考代码

n = int(input()) meetings = [] for _ in range(n): s, e = map(int, input().split()) meetings.append((s, e)) meetings.sort(key=lambda x: x[1]) ans = 0 last_end = -1 for s, e in meetings: if s >= last_end: ans += 1 last_end = e print(ans)

复盘要点:如果题目改成“最多能安排多少个不重叠区间”,那这里选<还是<=就要看题目定义。开会通常认为[0, 1][1, 2]是可以无缝衔接的,所以用s >= last_end。如果要求严格不重叠(首尾不能相接),就要改成s > last_end。这类细节在笔试里经常出现,建议拿到题目先标记清楚边界条件。

3.3 岛屿数量:DFS 递归与现场保护

题目描述:给定一个m x n的二维网格,'1'表示陆地,'0'表示水域。岛屿由相邻(上下左右)的陆地组成,请你计算岛屿的数量。m, n <= 200

输入示例

4 5 11000 11000 00100 00011

输出示例

3

解题思路:遍历整个网格,遇到一个'1'就把它所在连通块里的所有'1'都改成'0'(或者用visited数组标记),岛屿数量加一。这里我用 DFS 实现,因为代码量少,适合笔试。

参考代码

import sys sys.setrecursionlimit(1000000) def dfs(i, j, grid, m, n): if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] == '0': return grid[i][j] = '0' dfs(i + 1, j, grid, m, n) dfs(i - 1, j, grid, m, n) dfs(i, j + 1, grid, m, n) dfs(i, j - 1, grid, m, n) m, n = map(int, input().split()) grid = [list(input().strip()) for _ in range(m)] ans = 0 for i in range(m): for j in range(n): if grid[i][j] == '1': ans += 1 dfs(i, j, grid, m, n) print(ans)

复盘要点:DFS 的递归深度在这个题目里最多是m*n,最大 40000,Python 默认递归深度可能不够,所以我写了sys.setrecursionlimit。如果不想冒险,可以用栈模拟 DFS,或者换成 BFS。现场写的时候我还发现grid[i][j] == '1'这种判断在二维字符数组里很顺手,但如果是整数矩阵,记得不要写成双引号和单引号混用。这类题的核心在于“访问过就把值改掉”,避免重复计算。

3.4 找零问题:完全背包的一维优化

题目描述:给定不同面额的硬币coins和一个总金额amount,返回凑成总金额所需的最少硬币个数。如果无法凑成,返回-1。每种硬币数量无限,amount <= 10000

输入示例

3 1 2 5 11

输出示例

3

解题思路:使用一维数组的完全背包动态规划。dp[i]表示凑出金额i的最少硬币数,初始化为一个很大的数,比如float('inf')dp[0] = 0。遍历每种硬币,再遍历金额icoinamount,更新dp[i] = min(dp[i], dp[i - coin] + 1)。最终dp[amount]如果是无穷大,返回-1

参考代码

n = int(input()) coins = list(map(int, input().split())) amount = int(input()) INF = 10**9 dp = [INF] * (amount + 1) dp[0] = 0 for coin in coins: for i in range(coin, amount + 1): if dp[i - coin] + 1 < dp[i]: dp[i] = dp[i - coin] + 1 print(-1 if dp[amount] == INF else dp[amount])

复盘要点:这里最容易搞错的是内外层循环的顺序。如果是“每种硬币只能用一次”的 0/1 背包,内层要倒序遍历;但本题硬币无限,是典型完全背包,内层正序遍历才能让同一个硬币被多次使用。很多人死记硬背“正序还是倒序”,其实可以想一下:dp[i - coin]如果已经使用了当前硬币,正序遍历时还能继续基于它再选同一个硬币,就实现了无限取用。这样理解比背结论牢靠得多。

3.5 括号匹配:栈的边界处理

题目描述:给定一个只包含'('')''['']''{''}'的字符串,判断括号序列是否合法。字符串长度不超过10000。

输入示例

([{}])

输出示例

true

解题思路:用栈维护当前未匹配的左括号。遍历字符串时,如果是左括号就入栈;如果是右括号,判断栈顶是否是对应的左括号,不对应或者栈为空都表示不合法。遍历结束后,如果栈为空则合法,否则存在未匹配的左括号。

参考代码

s = input().strip() stack = [] pairs = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in '([{': stack.append(ch) else: if not stack or stack[-1] != pairs[ch]: print("false") break stack.pop() else: print("true" if not stack else "false")

复盘要点:Python 的for...else在笔试里用得好会很省事,else块在循环没有被break中断时执行,正好用来处理合法情况。容易忽略的细节包括:空栈时遇到)属于非法;最后栈里还有(也属于非法。我现场第一次提交时忘了if not stack,直接stack[-1]导致运行时错误。在线笔试遇到这类异常不会友好提示,所以边界判断一定要写在前面。

4. 在线笔试的输入输出与边界值陷阱

4.1 牛客的输入格式到底该怎么读

很多第一次用牛客做题的人最不适应的就是标准输入。LeetCode 给你一个函数,参数都传好了,但牛客要求你从标准输入里自己解析。以 Python 为例,我常用的模板是:

import sys def solve(): data = sys.stdin.read().split() # 按需求解析 data 列表 pass if __name__ == "__main__": solve()

sys.stdin.read().split()会把全部输入切分成一个个 token,好处是处理类如“第一行一个数n,第二行n个数”的格式时非常方便。缺点是如果要逐行处理带空格的字符串,就不能简单用split,这时候要配合input().strip()或者sys.stdin.readline

共享同一个input()sys.stdin.readline的区别在数据量大时很明显。对于万级以上的输入,input()底层调用的解释器逻辑偏慢,而sys.stdin.readline直接读一行,速度更快。我一般在大数据量题目里固定使用sys.stdin.readline

import sys input = sys.stdin.readline n = int(input()) arr = list(map(int, input().split()))

另外,读到的每行末尾可能带有\n,如果题目要求逐字符串处理,一定要strip()。如果目标字符串本身就是空行,strip()后得到空字符串,也要做好判断,避免对空字符串取下标时报错。

4.2 容易被忽略的边界值

编程题判题用例特别喜欢在边界上设陷阱。五模这套题里,我遇到的边界问题包括但不限于:

  • 字符串长度为 1:压缩循环里不会进入内部else,最后必须把最后一组字符写入结果。
  • 会议数量为 0:last_end初始值要小于任何合法开始时间,否则会漏选第一个会议。
  • 网格只有一行或一列:DFS 的四个方向里,有两个方向会越界,必须有边界判断。
  • 金额为 0:dp[0] = 0,此时不需要任何硬币,答案应该是 0,而不是-1
  • 括号序列为空:栈为空,合法,输出true
  • 输入行末有空格:如果用strip()处理,会丢失字符串内部的合法空格吗?如果题目要求保留空格,就不能对所有字符串无脑strip(),只能去掉末尾换行。

笔试时最冤的丢分不是不会做,而是没有把题目描述里的每一个“空”“重复”“0”当回事。我通常会在草稿纸上单独列一个边界值清单,写完代码后逐个代入验证。

4.3 超时与递归过深的处理方法

有些同学思路正确但 TLE(超时),常见原因有两个。第一个是不加判断的暴力枚举。比如最少硬币问题里,直接用递归穷举所有组合,金额稍微大一点就直接指数爆炸。第二个是 Python 递归爆栈,DFS 在最大网格上可能递归几万层,不主动提高递归上限会有RecursionError

超时的优化思路也分两种:

  • 对暴力枚举问题,先检查是否存在重叠子问题,有就想能不能用动态规划或记忆化搜索。
  • 对 DFS,如果递归深度不可控,就改成显式栈。显式栈写法稍微长一点,但不会受限于递归深度。
def num_islands(grid, m, n): dirs = [(1,0),(-1,0),(0,1),(0,-1)] ans = 0 for i in range(m): for j in range(n): if grid[i][j] == '1': ans += 1 stack = [(i, j)] grid[i][j] = '0' while stack: x, y = stack.pop() for dx, dy in dirs: nx, ny = x + dx, y + dy if 0 <= nx < m and 0 <= ny < n and grid[nx][ny] == '1': grid[nx][ny] = '0' stack.append((nx, ny)) return ans

单看代码量并不比 DFS 多多少,但稳定性更高。如果追求稳妥,我推荐这类搜索题在笔试环境里优先考虑显式栈或 BFS。

5. 模考之后最有效的复盘姿势

5.1 给错题打标签

模考不是做完对着答案看一遍就结束,而是要对错题做分类。我习惯把每道错题标记为四类之一:

  • 不会做:完全没有思路,需要补对应专题。
  • 会做但超时:算法复杂度有问题,需要优化。
  • 思路对但答案错:大概率是边界条件、输入输出、初始值的问题。
  • 代码对但提交失败:可能是环境差异,比如用了较新的 Python 语法,牛客判题机不认。

标签盖好以后,重点不是重新做一遍,而是针对每一类错因做一次专项训练。比如“边界条件出错”特别多,就集中刷一些数据范围很小的题,锻炼自己找边界的能力。

5.2 沉淀一套自己的代码模板

模考另一个作用是暴露你的“底层模板是否熟练”。代码模板不是死记硬背,而是把高频考点的基础结构先固化成肌肉记忆。我自己的模板库里有这么几块:

  • 输入输出模板(sys.stdin.readline
  • 二叉树先序/中序/后序遍历模板
  • DFS/BFS 的网格与图模板
  • 一维 DP 的完全背包/0/1背包模板
  • 区间贪心排序模板
  • 单调栈模板

有了这些模板,正式笔试里第一眼看到题目类型,就能快速搭建起代码骨架,把更多时间留给真正的思考。

5.3 后续刷题规划建议

如果你的目标是一个月后参加秋招或实习笔试,不建议漫无目的地在题库里乱刷。可以按阶段推进:

  • 第一周:数组、字符串、模拟、排序、二分查找。
  • 第二周:链表、栈、队列、哈希表。
  • 第三周:树、DFS/BFS、图。
  • 第四周:动态规划、贪心、回溯算法。

每周结束时,把牛客周赛或者模拟题做一遍作为检测。不要贪多,一天吃透两三道有价值的题,比一天刷十道但留下印象的题更有用。

我自己的体会是,牛客模考五模这套题集,难度不算变态,但每一道都踩在正式笔试的高频点上。特别是第四题最少硬币,当时我完全没有 DP 意识,用 DFS 硬搜,最后 TLE。后来把完全背包的内外层顺序彻底弄懂之后,这种小优化成了长在脑子里的东西。

最后再分享一个我在线上笔试里常用的土办法:所有提交前,在本地把题目里给的那个输入示例跑一遍,再自己造一个最小输入,比如只有一个元素、金额是 0、网格只有一行三列,把这些边界值都跑通,心里的底就足多了。

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

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

立即咨询