ARTICLE DETAIL

资讯详情

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

牛客模考2019一模编程题解析:字符串、数组与DP笔试技巧

牛客模考2019一模编程题解析:字符串、数组与DP笔试技巧 提起牛客模考很多人第一反应是“刷题刷到吐”。但2019年这套一模编程题集合我到现在还会翻出来重刷。原因是它不考偏题怪题考的净是字符串处理、数组模拟、基础动态规划这些笔试基本功。题目看着都不难但真正上了笔试环境能一次通过的人其实不多因为坑都藏在细节里边界条件、输入输出格式、空值处理、数据范围任何一个没想清楚就白费半小时。这套编程题集合特别适合两类人一类是准备校招笔试、想在牛客上练手找感觉的同学另一类是刚学完编程基础、想检验自己能不能独立写完整程序的新手。我的建议是不要只收藏不练习直接照着ACM模式去写用标准输入输出跑一遍再对照解析看自己漏在哪。本文会把整套题的考点分布、典型题目的完整解题思路和代码、笔试现场的排查技巧全部拆开讲清楚内容偏实操你可以直接拿去用。1. 整体考点拆解与备考定位1.1 2019一模编程题的题型分布与用意这套题一共八道左右按难度看是阶梯式上升。前两三道属于“送分题”考字符串反转、数字统计、字符计数主要目的是让你进入状态中间三四道开始上数组操作和模拟比如数组去重、约瑟夫环、查找只出现一次的数字最后两道才开始有动态规划的影子例如爬楼梯、最长连续上升子序列这类经典入门DP。为什么这么安排牛客模考模拟的是真实校招笔试环境真实笔试第一题如果太难很多人直接心态崩溃整场考试就废了。所以前面放简单题既是让你热身也是考察你在压力下能否快速写出干净代码。而后面的DP题考察的不是你背了多少模板而是你能不能把问题抽象成状态转移。这种能力不是临时刷几十道题能补上的得靠平时积累。我当时做完这套题最大的感受是它跟LeetCode那种纯算法题不一样它更贴近国内笔试的“ACM风格”。题目描述里会带输入输出格式、数据范围你必须自己处理输入解析自己处理多组测试数据。很多在IDE里写惯了函数的人第一次接触这种模式会很不适应觉得“我逻辑明明对为什么判题不过”。原因往往就是输入输出没处理好。1.2 为什么说这类题目最考验真实编程功底我在各个技术社区见过不少刷题量很大的人LeetCode能稳定做中等题但一上牛客笔试就翻车。原因很简单牛客的编程题是黑盒判题它不会告诉你哪组数据错了只给你一个“通过率0%”。这种情况下你能不能靠自己的经验定位到问题本身就是编程能力的一部分。这套一模题正好把这种“笔试真实感”拉满了。你写的程序不仅要算得对还要抗得住边界输入。例如字符串的连续空格、空字符串、超大整数、数组长度只有1的情况。每年都有人在这些地方翻车不是不会做而是没养成“先想边界再写代码”的习惯。另外这套题还有一个隐性的考察点代码风格。判题系统虽然不看你代码好不好看但笔试结束后如果你进入面试环节面试官可能会翻看你当时的代码。变量名乱写、逻辑堆成一坨、没有注释即便你AC了印象分也会打折扣。所以我在下面的解析里会尽量把代码写得规范一些方便你也养成好习惯。1.3 备考资料与刷题顺序建议如果你现在手头没有完整题单我的建议是先把经典基础题刷透再上这套模考。最近几年Python等级考试一级里大量出现的题型——比如字符串反转、数字统计、循环模拟——跟这套一模题的重合度非常高。如果你正在准备Python考级拿这套题练手也完全合适因为两者考的底层能力是一样的读懂需求、拆解问题、把逻辑翻译成代码。刷题顺序我推荐这样安排先独立AC一遍卡住超过20分钟就看提示但看完提示必须自己重写一遍不能直接抄代码。AC之后再看有没有更优解比如这题用暴力法能过但数据范围变大之后会不会超时把每一道题都做“透”比盲目刷三套题管用得多。这套一模题总量不大正好适合精刷。2. 字符串与模拟类题目的高分写法2.1 题目一反转字符串中的单词顺序这道题是整套题里的典型送分题但也是翻车重灾区。题目描述大概是给出一句话单词之间用空格分隔要求输出单词顺序反转后的结果。注意不是把每个单词的字母反转而是把单词的顺序反转比如输入“I am a programmer”输出“programmer a am I”。很多人看到这题第一反应是用split按空格切分然后翻转列表再join。逻辑确实对但如果你直接写s.split( )就会在“连续多个空格”这个测试用例上栽跟头。牛客判题用例经常故意给s I am a programmer中间有两个空格你用单空格切割会出现空字符串元素最后输出多出一堆空格判题直接不给过。我当时第一次提交就是这个问题后来养成了一个习惯凡是字符串切分先确认题目说的是“空格分隔”还是“单个空格分隔”。描述模糊时优先用split()无参数它会自动按任意空白字符切分并过滤空串。这道题用无参split()就能干净解决。代码实现很简单s input().strip() words s.split() result .join(words[::-1]) print(result)这里用strip()去掉首尾多余空格再用无参split()切出单词列表最后倒序拼接输出。整段代码不到五行。但注意Python的split()默认处理空白字符包括空格、Tab和换行这正好应对了题目的各种隐藏空格陷阱。2.2 题目二统计字符串中的数字并求和这题也很典型给你一个包含字母和数字的混合字符串需要提取出里面所有连续的数字子串并把它们加起来。例如输入abc123def45gh6输出174123456。这题考的是对字符串遍历的敏感度。常见做法是维护一个“当前累积数字”的变量cur遇到数字字符就用cur cur * 10 int(ch)遇到非数字字符就把cur加到总和并清零。循环结束后还要再判断一次cur是否为0否则末尾的数字会漏加。这一步非常关键经常有人在这里丢分。我自己写的时候为了更稳直接用正则表达式import re s input().strip() numbers re.findall(r\d, s) total sum(int(num) for num in numbers) print(total)re.findall(r\d, s)会把所有连续数字子串都找出来然后逐个转成整数求和。正则的优点是省心不容易漏边界缺点是有的人对正则不熟面试时被问到底层实现会答不上来。所以我建议两种方案都掌握笔试追求速度用正则平时练习建议手写遍历这样你对字符处理的敏感度才会真正提升。2.3 字符串类题目的细节避坑指南字符串题目看起来简单实际上有很多常见的坑我在这套题上踩过的、帮别人排查过的整理成了一张速查表你可以直接照着自查。坑点错误示例正确做法切分后出现空字符串s.split( )遇到连续空格使用无参split()末尾数字漏加遍历循环结束后没处理cur循环后再判断一次cur大小写干扰统计字母时没统一大小写先lower()转换再处理输入含首尾空格直接对原字符串操作先strip()去掉首尾空白这些细节看着小但笔试现场就是靠这些拉开差距。同样的思路别人AC了你卡在0%区别往往不在算法而在这些“脏活”处理上。我见过太多人因为字符串末尾多了一个换行符比对结果死活不对最后发现是input()没strip()。这种时间浪费完全可以避免。3. 数组、查找与数学规律题的解法思路3.1 题目三找出数组中唯一出现一次的数字这道题描述很经典给定一个非空整数数组除了某个元素只出现一次以外其余每个元素均出现两次找出那个只出现一次的元素。要求算法尽量高效不额外开辟大空间。很多人的第一反应是用哈希表统计频率再找出值为1的键。这种做法能过但被问到“你能不能不用额外空间”时就会卡住。这道题的最优解是异或运算相同的数字异或为0任何数字和0异或还是它本身所以把数组中所有数字依次异或一遍出现两次的都会抵消剩下的就是那个只出现一次的数字。我第一次看到这个解法时觉得太巧妙了后来做题多了才发现异或处理“成对抵消”是笔试里的高频套路。代码写出来异常简洁nums list(map(int, input().split())) result 0 for num in nums: result ^ num print(result)这里只有一个循环时间复杂度O(n)空间复杂度O(1)。我能理解为什么笔试爱考这种题——它考察的不仅是你会不会写哈希表而是你对位运算有没有概念。如果你只会暴力解法很多后续题目都会做得非常吃力。3.2 题目四约瑟夫环的模拟实现约瑟夫环是这套题里少有的“模拟”题也是最容易出现“看着会做一写就错”的题。题目描述通常是n个人围成一圈从第1个人开始报数报到m的人出圈然后从下一个人重新报数问最后剩下的人的原始编号是多少。最直观的解法是模拟整个过程用列表存所有人用指针移动每报到m就弹出一个人。Python里用列表模拟这个过程代码比较短但要注意索引的计算。每弹出一个元素后列表长度减一指针要相应回退否则会跳过或重复处理元素。这里我直接给两种写法。第一种是用列表模拟适合数据量小的情况n, m map(int, input().split()) people list(range(1, n 1)) idx 0 while len(people) 1: idx (idx m - 1) % len(people) people.pop(idx) print(people[0])第二种是数学递推法不使用列表直接通过状态转移得出最后编号n, m map(int, input().split()) res 0 for i in range(2, n 1): res (res m) % i print(res 1)第二种解法代码更短但需要你理解约瑟夫环的递推公式。笔试时如果数据范围小用第一种模拟就够了如果n高达十的六次方甚至更大就必须用递推法否则会超时。这道题能不能拿满分取决于你能不能判断出当前数据范围该选哪种方案。3.3 数组边界与特殊输入的处理经验数组类题目最常见的错误来源是索引越界。这套题里我遇到过n1甚至n0的边界用例。有些同学一上来就写if nums[1] nums[0]数据长度为1时直接崩掉。我的习惯是拿到数组题先问自己三个问题——数组为空怎么办数组长度为1怎么办最大值和最小值相等怎么办这三个问题想清楚再动手写代码能减少一大半的错误提交。还有一个经验是在牛客这种ACM模式下数组输入可能是同一行也可能是多行题目描述会说清楚。如果没把握最好用sys.stdin.read()一次性读取全部数据再拆分解析这样无论换行方式怎么变都不会出错。下面是一个通用读取模板import sys data list(map(int, sys.stdin.read().split()))这种读法在处理多组输入时尤其好用它可以忽略所有空格和换行直接把整份输入转成整数列表自己按需切片。使用这个模板后我再也没有因为“换行符导致读取出错”这类问题浪费过时间。4. 动态规划题型的识别与应对4.1 题目五爬楼梯的经典解法与空间优化爬楼梯属于几乎每套笔试题都会出现的入门动态规划题。题目很直观你正在爬楼梯需要n阶才能到顶每次可以爬1阶或2阶问有多少种不同的方法爬到楼顶。这道题最直接的思路是递归但纯递归会重复计算大量子问题n稍大就直接超时。正确做法是动态规划定义 dp[i] 表示爬到第i阶的方法数那么第i阶只能从第i-1阶跨一步或者从第i-2阶跨两步到达所以状态转移方程为 dp[i] dp[i-1] dp[i-2]。初始条件 dp[1]1dp[2]2。按照这个思路代码很清晰n int(input()) if n 2: print(n) else: a, b 1, 2 for _ in range(3, n 1): a, b b, a b print(b)这里我没有开整个dp数组而是用两个变量滚动更新把空间复杂度从O(n)降到O(1)。很多刷题指南都会强调这种优化因为当n达到十万甚至百万时开数组的写法和滚动更新的写法内存占用完全是两个量级。4.2 题目六最长连续上升子序列的遍历技巧这题在整套题里属于“看着像动态规划其实普通遍历就能解决”的题型。题目问的是给定一个整数数组找到其中最长连续上升子序列的长度。注意关键词是“连续”这意味着你不需要回溯重新选择。我见过不少同学一看到“上升子序列”就想上最长递增子序列的动态规划模板dp数组开了、两层循环写了结果数据一大就超时。其实“连续”这两个字已经把难度降了很多你只需要遍历数组一遍如果当前元素比前一个元素大当前长度加一否则从1重新开始计数同时更新最大长度。nums list(map(int, input().split())) if not nums: print(0) else: max_len 1 cur_len 1 for i in range(1, len(nums)): if nums[i] nums[i-1]: cur_len 1 max_len max(max_len, cur_len) else: cur_len 1 print(max_len)这题之所以值得讲是因为它提醒你做题前一定要先读清楚题干。是“连续”还是“非连续”解法复杂度天差地别。如果你一上来就套模板不光代码复杂还可能超时。笔试现场时间有限看清题目条件再动手比盲目刷题重要得多。4.3 从零开始搭建DP思路的思考路径很多人对动态规划有种莫名的恐惧一看到“DP”两个字就觉得自己不行。这套一模题里的DP其实都特别基础刚好适合用来建立正确的思考路径。我的经验是分四步走第一步明确状态想清楚 dp[i] 代表什么第二步找转移方程想清楚当前状态能从哪些状态推导出来第三步定初始条件把最小规模的解直接写死第四步确认遍历顺序是从小到大还是从大到小。如果你做一道题想不过来建议先在纸上画出dp数组的变化过程。比如爬楼梯那题你手写出 dp[1]1, dp[2]2, dp[3]3, dp[4]5看到斐波那契数列的规律后自然就理解了为什么要用前两项求和。我在学习DP的那个阶段靠的就是这种“笨办法”——不急着写代码先手动算几个例子公式就藏在这些例子里面。5. 笔试现场的时间分配与问题排查5.1 做题顺序建议先捡软柿子捏整套题做下来我的强烈建议是不要按题号顺序硬刚。先把所有题都快速扫一遍标记出哪些是前几分钟就能拿下的送分题哪些是需要推理的模拟题哪些是DP题。然后按“送分题 - 模拟题 - DP题”的顺序做。这样做的目的很现实先把能拿的分稳稳拿住再留大块时间啃硬骨头。有些同学喜欢从第一题按顺序做到最后一题一旦前面卡住了后面简单题也没时间做。这种策略在笔试里非常不划算。牛客判题是按通过率给分的哪怕你最后一道大题的用例通过一半也比前面一道题都做不出来强。我自己的节奏是总共120分钟前40分钟搞定三道送分题中间60分钟攻模拟题和中等题最后20分钟留给DP尝试。如果你基础稍弱可以适当调整比例但“先做能拿分的”这个大原则不要变。5.2 常见运行错误与排查技巧速查表笔试过程中最烦的不是“题目不会做”而是“程序莫名其妙报错”。我把这套一模题里最容易出现的问题类型整理了一下你在本地测试时可以直接对着看错误类型可能原因排查方向IndexError列表越界检查循环边界尤其是n1的边界Time Limit Exceeded循环嵌套过深是否有O(n²)的暴力解可以优化NumberFormatError输入包含非数字内容检查split()后是否需要过滤空串Wrong Answer逻辑对但边界漏判针对输入最小值、最大值、空值测试MemoryError开了过大的数组改为滚动变量或减少存储维度这套表是我当年刷完题后自己总结的之后每次笔试前都会扫一眼比临时翻笔记管用得多。尤其是“Wrong Answer但实在查不出错”的情况九成都是边界条件没覆盖到。这时候不要干瞪眼试着往输入里塞几个极端值比如空字符串、0、负数、超大数问题通常马上浮出水面。5.3 实测有效的查错小习惯我分享一个自己的笨办法虽然土但救了我很多次写完代码后先不要立刻提交自己手动在注释里列出三组测试用例分别是普通输入、边界输入、极端输入然后逐组推演一遍结果。比如做字符串反转那题我就在草稿纸上写“输入空字符串”“输入全是空格”“输入只有一个单词”然后确认代码输出都符合预期再放心提交。这个方法看起来多花了几分钟但它能大幅减少“提交一次错一次”的循环。牛客笔试有的场次提交错误会有罚时反复提交错误答案成绩会受到很大影响。与其赌运气不如提交前自己把雷扫一遍。还有一个小细节写完代码后整体读一遍重点看if/else的条件分支是否可能互相覆盖while循环会不会死循环数据范围大的时候int是否够用。这些检查加起来不到五分钟却能避开很多常见失分点。5.4 把模考题变成自己的题库这套一模题最值得借鉴的价值不是题目本身而是它揭示的考点分布规律。你做完之后完全可以按同样的思路去整理自己的错题库每道题记录四栏——题目类别、我的错误原因、正确解法、同类题扩展。比如约瑟夫环这道题你可以把解法整理成“模拟法 递推法”的对比笔记再去找其他“报数出圈”类题目做两遍直到闭着眼都能写出来为止。我自己的习惯是每套模考做完后都会把代码按专题存到一个文件夹里并标记做题日期和AC状态。两个月后再重新打开这些文件夹看自己当时写的代码能非常直观地感受到进步。这种积累方式比刷完就忘有效得多。刷题不是目的能把每道题背后的思考方式消化成自己的东西才是做这套一模编程题集合的真正意义。
返回列表