如果你也在准备华为机试,我建议你把模拟题系列认真刷一遍,特别是这套题。我前几天刚把编程模拟题6完整过了一遍,说实话,这套题比很多网上流传的真题更值得做。它把字符串处理、滑动窗口、动态规划这几个高频考点全部覆盖到了,而且题目设置得很贴近真实机试的节奏。无论你是刚开始接触华为OD机试,还是已经刷过一些题想查漏补缺,这套模拟题都能帮你校准备考方向。
我刷完之后最大的感受是:华为机试的难点不在题目本身有多难,而在于你在限定时间内能不能又快又稳地把三类题做出来。模拟题6设计得很有代表性,所以我想把整套题的拆解思路、代码实现和踩坑记录完整分享出来,给后面备考的朋友一个参考。
1. 这套模拟题背后的考核逻辑:先看清机试在筛什么人
在讲题目之前,必须先搞清楚华为机试为什么这么考。很多人一上来就刷题,刷了两个星期还是没底,就是因为没看懂出题人的意图。华为机试本质上是在筛选两类能力:第一类是代码基本功,你能不能把思路干干净净地落地成可运行的代码;第二类是问题建模能力,面对一个带业务包装的描述,你能不能快速识别出它到底在考哪个经典算法。
华为OD机试采用新系统之后,很多考场是双机位C卷模式,也就是说除了电脑屏幕之外,还有第二机位监控整个考试环境。卷子分为100分和200分两档题,总分一般是400分。150分是大多数岗位的及格线,但这个线不是固定的,跟你投的部门、岗位等级都有关系。模拟题6的题型分布就是按这个标准来设计的:两道100分题,一道200分题,整体难度曲线是先易后难。
很多人容易犯一个错误,就是觉得100分的题可以随便敷衍,把时间都留给200分的压轴题。我个人的建议恰恰相反,100分题必须拿到手,而且尽量拿满分。因为200分题往往存在一种情况:你思路对了,但某一个边界条件写错,导致大量测试用例挂掉。这时候如果100分题再因为粗心丢分,总分就很危险了。模拟题6的前两道题,就是用来练这种"稳扎稳打"的手感的。
另外还要注意,华为机试的代码是提交到在线评测系统里,不是像面试那样口头讲讲思路就行。这意味着你对输入输出格式的处理必须非常严谨。很多人在本地跑得好好的,一提交就报错,八成是卡在输入输出的细节上。模拟题6里也有些这类隐藏的坑,后面我会单独拿出来说。
从这个角度看,刷模拟题不仅仅是在刷算法,更是在刷一套完整的考试操作流程。等你上了真考场,你会发现所有操作都已经肌肉记忆了,真正要留给大脑思考的时间,全花在200分那道题上,这才是最终合理的节奏。
2. 三道题的考点拆解:字符串、滑动窗口、贪心里的门道
模拟题6的三道题,我一个个拆开讲。每一道题我都会说明它对应的考点、出题人想考你什么、以及最容易被忽略的细节。这样你以后再碰到同类题,就知道往哪个方向想了。
2.1 字符串解压:不能只想着"看到字母就拼字符串"
第一题是一个字符串解压问题,输入一段压缩后的字符串,比如"a3b2c1",要求输出解压后的完整字符串,也就是"aaabbc"。压缩规则很好理解:每个字符后面跟一个数字,表示这个字符重复几次。
我看完题的第一反应是,这题太简单了,直接遍历,遇到字母就记录,然后读数字,循环拼接。但如果你也这么想,大概率会掉进一个坑里:题目没有说数字一定是一位数。也就是说,输入完全可能是"a10b2",这种情况下你要解压出10个a。很多人在这一步直接用单个字符读数字,结果遇到多位数就出错。
正确处理方式是边读边累加。读完字母之后,用while循环不断读取数字字符,每读一位就把之前的数字乘10再加上当前位。这样无论数字是几位都能正确处理。还有一个隐藏的边界情况是"a0b2",意思是字符a重复0次,也就是什么都不输出。这种用例看起来像故意刁难,但其实考的就是你思维的严谨程度。
这道题的考点本质是字符串的解析与重建,关键词是"解析"。华为机试里大量题目都长这个样子:给你一个格式比较规整的字符串,先按照某种规则解析,再按照另一种规则输出。你不需要用什么复杂的算法,纯粹考察对字符串下标、拼接、ASCII转换这些基本功的熟练度。代码量不大,但要求一次写对。
我刷题的时候习惯先写最朴素的版本,再回头检查边界。朴素版本就是遍历、解析、拼接,不搞花哨优化。这道题里的所有细节,比如多位数、0次重复、空串输入,其实都可以通过自测用例覆盖。后面我会专门列一个自测用例清单。
2.2 翻转0的最长连续1子数组:不要一上来就改原数组
第二题是一道经典的滑动窗口变体,题面大概是:给你一个由0和1组成的数组,最多可以翻转k个0为1,求翻转后最长的连续1子数组的长度。这个题目有很多名字,有人叫"最大连续1的个数",有人叫"滑动窗口内的0计数"。
我看到这道题的第一个念头是:能不能真的把数组里的0改成1?反正最多k个,翻转之后求最长连续1。但这个思路在编程实现上很麻烦,因为你不知道应该翻哪几个0,要试的窗口很多。正确的做法是:不要真的去翻转,而是维护一个滑动窗口,让窗口内0的个数始终不超过k,窗口的长度就是当前可能的连续1长度。
具体思路是这样的:用两个指针left和right维护窗口,right不断向右扩展。每遇到一个新的0,就把窗口内的0计数器加一。如果0的个数超过了k,说明窗口太大了,left需要向右收缩,直到0的个数重新小于等于k。在收缩的过程中,如果left离开的是一个0,计数器要减一。每次调整完窗口之后,用当前窗口长度去更新答案。
这个思路的本质是把"最多允许k个0"这一约束,转化成"窗口内0的数量上限"。我在做模拟题6的时候,差点犯一个错误,就是只更新了right没有维护left,结果窗口越滑越大。记住,滑动窗口题最核心的部分就是搞清楚left什么时候收缩,以及收缩的时候需要更新哪些统计信息。
另外,这道题的时间复杂度是O(n),空间复杂度是O(1),在机试环境里属于非常标准的解法。如果有人在面试或者讨论里说这题可以用动态规划做,也不是不行,但没有滑动窗口直观。机试就是求稳,哪个容易理解不容易写错,就用哪个。
2.3 最少跳跃次数:贪心还是动态规划,得想明白
第三题是整张卷子的压轴,描述大概是:给你一个数组arr,每个元素表示从当前位置最多可以往后跳多少步,假设你从下标0出发,问到达最后一个位置最少需要跳几次。数组保证一定可以到达末尾。
看到"最少"两个字,很多人第一反应是动态规划。确实可以定义dp[i]表示到达第i个位置的最少跳跃次数,然后从前往后扫描,转移方程大概是dp[i+j] = min(dp[i+j], dp[i]+1),其中j的取值范围是1到arr[i]。但这样做的时间复杂度是O(n乘以最大步长),在数据量大的时候可能会超时。
更优的解法是贪心。思路是维护三个变量:当前跳数step,当前这一跳能到达的最右边界curEnd,以及从现在位置出发能到达的最远位置curFarthest。每遍历一个位置,就更新curFarthest为max(curFarthest, i + arr[i])。当i到达curEnd时,意味着这一跳已经走到了尽头,必须再跳一次,step加一,同时把curEnd更新为curFarthest。如果curEnd已经覆盖了最后一个位置,就提前退出。
这个贪心的关键在于"什么时候必须跳"的判断。很多讲解会说curEnd保存的是当前步数内能到达的最远位置,但代码一多就容易绕晕。你可以换个方式理解:你在某一段范围内可以选择任意一点作为起跳点,起跳点能去的最远位置,决定了下一跳的边界。当你在当前跳范围内遍历完所有点时,你实际上已经找到了这一跳范围内所有点能到达的最远位置,所以必须跳出去,这就是贪心最优的体现。
我在模拟题6里选择的解法是贪心,因为它在O(n)时间内解决,而且边界条件相对清晰,只有curEnd和curFarthest两个变量需要维护。相比之下,动态规划虽然思路更好想到,但循环嵌套加上转移条件,反而容易在机试的紧张状态下写错。这里有个经验:200分题不要纠结于最优解是不是贪心,如果你只能稳定写出动态规划,那就用动态规划,至少能拿大半分数。但平时训练还是要两种方法都掌握。
3. 参考实现与自测用例:直接从能跑的代码里学
光讲思路,不贴代码,等于没说。我把三道题的参考实现都写出来,用的都是C++,因为华为机试的主流语言之一就是C++。你写成Java或Python,思路完全一样,只是语法不同。每段代码我都会标注几个关键点,方便你对位检查。
3.1 字符串解压的边界处理实现
#include <iostream> #include <string> using namespace std; int main() { string s; cin >> s; string res; int i = 0; while (i < (int)s.size()) { char c = s[i++]; int num = 0; while (i < (int)s.size() && isdigit(s[i])) { num = num * 10 + (s[i] - '0'); i++; } res.append(num, c); } cout << res << endl; return 0; }这段代码的关键就是内层while处理多位数。我特别提醒一下,res.append(num, c)这个函数,如果num为0,就什么都不加,这正好符合题目中"重复0次"的语义。如果你用循环来拼接,记得加上是否num为0的判断,否则可能多输出一次。
3.2 滑动窗口翻转0实现
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int n, k; cin >> n >> k; vector<int> nums(n); for (int i = 0; i < n; i++) { cin >> nums[i]; } int left = 0, right = 0; int zeroCount = 0; int ans = 0; while (right < n) { if (nums[right] == 0) { zeroCount++; } while (zeroCount > k) { if (nums[left] == 0) { zeroCount--; } left++; } ans = max(ans, right - left + 1); right++; } cout << ans << endl; return 0; }这里的输入格式我按"第一行n和k,第二行n个数"来处理,实际题目可能不同,但核心是窗口内0的计数。我写这题的时候特别留意了left收缩的时机:必须在zeroCount超过k之后才开始收缩,而且收缩要持续到zeroCount小于等于k为止,不是只收缩一次。
3.3 最少跳跃次数贪心实现
#include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; vector<int> arr(n); for (int i = 0; i < n; i++) { cin >> arr[i]; } if (n <= 1) { cout << 0 << endl; return 0; } int step = 0; int curEnd = 0; int curFarthest = 0; for (int i = 0; i < n - 1; i++) { curFarthest = max(curFarthest, i + arr[i]); if (i == curEnd) { step++; curEnd = curFarthest; if (curEnd >= n - 1) { break; } } } cout << step << endl; return 0; }注意循环只走到n-2,也就是最后一个位置的前一个位置。因为最后一个位置是终点,不需要再从这里起跳。如果n=1,一开始就在终点,答案就是0。这个提前返回很容易漏掉。
3.4 自测用例清单:反复使用,直到形成条件反射
我把这三道题应该测的用例全部列出来。在正式机试时,写完代码不要急着提交,先花两分钟把这类用例在脑子里过一遍。
| 题目 | 用例 | 预期输出 | 说明 |
|---|---|---|---|
| 字符串解压 | a3b2c1 | aaabbc | 基础用例 |
| 字符串解压 | a10b2 | aaaaaaaaaabb | 多位数压缩 |
| 字符串解压 | a0b2 | bb | 0次重复 |
| 字符串解压 | ab2 | abb | 数字为1时不下标混乱 |
| 滑动窗口 | 数组 1,1,0,0,1,1,k=1 | 4 | 0计数边界 |
| 滑动窗口 | 数组 0,0,1,1,0,0,k=0 | 2 | k为0时等价于直接求最长连续1 |
| 滑动窗口 | 数组全1,k=3 | n | 全1场景 |
| 最少跳跃 | 2,3,1,1,4 | 2 | 经典用例 |
| 最少跳跃 | 1,1,1,1 | 3 | 每步跳1 |
| 最少跳跃 | 3,2,1,0,1 | 2 | 有0步的场景,注意能否到达 |
这些用例的价值不在于多,而在于覆盖每一种可能的边界。以我的经验来看,机试中70%的常见错误都可以通过建立这套自测体系提前拦截掉。
4. 从模拟题到真题的迁移:考点不变,只换包装
华为机试有一个非常明显的特点:同一类考点,每年换着各种业务场景往外抛。你如果只记住了某一道题的题面,下一次遇到就不会做,那就等于白练。刷模拟题6真正的价值,是帮你总结出一套"题型迁移地图"。
我刚把三道题的考点抽象一下:
字符串解压:属于字符串解析类。相似题有:IP地址转整数、日志格式解析、压缩后的解压计数、括号匹配里的表达式值计算。华为特别喜欢出这种"给你一段格式特定字符串,按规则转换"的题。
翻转0求最长连续1:属于滑动窗口类。相似题有:无重复字符的最长子串、定长子数组的最大平均值、最小覆盖子串、长度最小的子数组。滑动窗口的核心模式就是left和right两指针,加上一个窗口内统计量。
最少跳跃次数:属于贪心/区间覆盖类。相似题有:会议室安排、无重叠区间、分发饼干、加油站。这类题最常见的就是找最值,而且要证明贪心选择是对的。
迁移的关键是"识别信号词"。看到"连续""子数组""最多""最少"这些词,就应该在脑内亮起对应的算法灯。比如题目说"最多可以修改k个",大概率是滑动窗口;说"最少跳几次""最少步数""最少硬币",大概率是动态规划或贪心。信号词不是100%准确,但至少能帮你快速锁定一个候选方向。
我在辅导别人的时候,经常让他们做一个动作:每刷完一道题,不要急着看下一题,停下来写一行注释——这道题如果我换个场景,会变成什么?比如说,字符串解压的题面换成"给出一段加密后的用户ID规则,还原原始ID",考点还是一样的。这样练多了之后,考场上读到题干的第三句话,你已经能感觉到这道题在考什么了。
模拟题6之所以有参考价值,就是因为它每一道题都选得很典型,属于那种"出题人只要改场景就能再出十道"的底子题。把底子题的解法吃透,其实是在用一题的时间复习十题。
5. 机试环境操作细节:输入输出、自测和提交流程的坑
这部分是很多人忽略但最容易丢分的地方。算法题你会做,思路也对,但操作环节出问题,同样功亏一篑。我整理了一些实际机试中会遇到的隐性坑,模拟题6的练习过程也暴露了其中几个。
首先是输入格式。华为机试的输入一般是标准输入,可能有多个测试用例,也可能只有一个。你要根据题目描述判断是否存在多组输入。模拟题6三道题都是单组输入,但真实考试中,有些题的输入行数或者格式会变化。一道保险的做法是:先读取第一行,确认数据规模,再根据规模读取后续内容。不要假设输入永远在一行里,更不要假设每个数字一定用空格分隔,有可能换成逗号或者分号。
其次是输出格式。多一个空格、少一个换行,都可能导致Wrong Answer。有些题目要求输出结果保留两位小数,有些要求不换行输出,还有些要求按字典序排序后输出。这些细节在题目描述里通常写得很清楚,但考场上容易忽略。我建议用最快的速度把题目描述扫一遍,标注输出要求,再开始写代码。
然后是自测。华为机试的在线评测系统通常支持你自己输入测试用例,也就是你可以在提交前先用自己的数据验证代码。模拟题6的代码写完之后,我要么用题目给的示例输入跑一遍,要么用上一节列出的自测用例跑一遍。这个过程花费的时间不超过五分钟,但能避免很多因为边界条件导致的全盘崩溃。
最后是提交策略。如果一道题你写了很久还是不对,不要死磕。200分题如果做不出来,至少先把暴力解法写上,哪怕只能过一部分用例,也能拿到一部分分数。机试是按用例给分的,不是零分或者满分两个选项。我见过很多人在第三题上死磕到底,结果第一二题的用例没有充分验证,送了不少分。合理的策略是:先把三题都做出来一版,再回头优化没通过的用例。
还要提醒一点的是代码规范。虽然机试的评测系统不检查你的代码风格,但清晰规范的代码能让你自己在调试的时候更快发现问题。变量命名尽量有意义,比如windowStart、maxLen,而不是a、b、c。遇到要调试的中间结果,善用cerr或者print语句。有些人觉得print会影响性能,但其实在自测阶段无所谓,提交之前清理掉就行。
6. 备考冲刺建议:实战后的复盘与节奏管理
最后聊点备考节奏上的体会。模拟题6刷完之后,我的建议是不要马上刷模拟题7,而是花一两天时间把这三道题重新默写一遍。默写的意思不是背代码,而是在不参考任何资料的情况下,从头开始写出可以运行的代码。这个过程能真正检验你到底是"理解了"还是"看懂了"。看懂了和能写出来之间,隔着一条巨大的鸿沟,只有亲手写才能跨过去。
默写完之后,我还会做一件让很多人觉得多余的事:给每一道题写一份错误分析报告。格式很简单,哪个地方卡住了、哪个边界没考虑、哪个函数用错了、为什么错。比如我在模拟题6里犯过一个错,字符串解压时把isdigit直接用在s[i]上但忘记i的类型转换,导致char强转成负数,触发未定义行为。这种东西如果不记录,下次碰到同样的问题,还是会栽。
备考的时间规划方面,我建议把最后一周分成两段。前半段,刷过去的错题和模拟题中没做出来的题目,每个题型至少过两遍。后半段,每天上午固定做一套完整的模拟题,严格按照考试时长来,中间不中断,训练考场的注意力分配。晚上不做新题,只复盘当天的错误和超时点。这不仅是在练算法,也是在做考试节奏演练。
最后的最后,说点心态上的事情。华为机试的门槛没有想象中那么高,但它确实会筛选掉那些代码功底不扎实和心态不稳的人。模拟题6让我比较欣赏的一点是,它的难度梯度非常合理,至少不会一道题做到一半就让你想放弃整场考试。如果你刷到某道题卡了很久,别急着怀疑自己,先跳过,把能拿的分都拿到,再回头做。这个策略听起来很朴素,但我在真考场上体会到过它的价值。
我用这套方法刷完模拟题6,再回顾整个机试准备过程,最大的收获是意识到:模拟题的"模拟"二字,其意义不只是模拟题目,更是模拟一场真实考试的完整链路——审题、建模、编码、自测、提交策略。很多人把刷题窄化成了做题,其实丢了更大的目标。希望这篇拆解能帮你在备考路上少走一些弯路,用更少的题量拿到更稳的分数。