☰
华为机试模拟题7复盘:字符串、滑动窗口、动态规划与任务调度实战解析
2026/10/10 8:57:04 网站建设 项目流程

前几天我把手头那套《华为机试编程模拟题7》从头到尾限时撸了一遍,两个半小时,四道题,全程盯着屏幕像在打仗。做完之后最强烈的感受就一个:机试这东西和平时刷题真的是两码事。平时你在 LeetCode 上慢慢想,没人催你;到了机试页面里,时间一滴滴走,输入输出的格式问题、结构体的堆法、边界条件的判断,全部压在同一段时间里,稍微一慌就全乱套。

这篇文章不聊虚的,就把我这套模拟题7的完整复盘写出来。四道题分别覆盖了字符串处理、滑动窗口、动态规划和流程模拟,都是机试里的高频方向。我会把每道题的读题思路、代码实现、易错点都拆开讲,后面再聊一聊不同基础怎么安排刷题路线,以及我踩过的坑。如果你也在准备华为机试或者类似的招聘机试,这篇文章应该能帮你少走不少弯路。

1. 华为机试到底在考什么:规则与核心考点

1.1 考试规则和评分机制

先说清楚规则,不然复习方向容易跑偏。不同批次、不同岗位的机试安排会有些差异,但大体框架是稳定的:时长一般在一个半小时到两个半小时之间,题目数量通常是三到四道,难度分布整体上前易后难。很多人第一次考的时候没概念,以为像学校考试一样按点给分,其实机试的评分机制普遍是按测试用例的通过比例给分。

这个机制带来的直接结论是:哪怕一道题你没法拿到满分,只要把暴力版本写出来,能过多少用例就捞多少分。我在模拟题7的第三题里就亲身体会到了这一点——动态规划想不出优化解法,先把 O(n^2) 的版本写上,至少能覆盖掉大部分常规用例。所以备考阶段就要养成一个习惯:永远不要留白,暴力解也是分。

关于语言选择,机试一般支持 C/C++、Java、Python、Go 这些主流语言。我的建议是优先选自己最熟的,但如果你水平都差不多,Python 在编码速度上确实有优势,尤其字符串处理和模拟类题目写起来比 C++ 短不少。不过用 Python 要注意运行效率,数据范围到 10^5 级别,纯 Python 的 O(n^2) 算法很容易超时,得提前想好替代方案。另外,Python 的递归深度默认只有 1000,涉及 DFS 的题目要么改迭代,要么在开头加 sys.setrecursionlimit,这个细节我后面还会细说。

1.2 核心考点分布与选择思路

我自己把网上能看到的华为机试题目和各类模拟题做了个粗略统计,按出现频率大致排了个序:

考点方向出现频率典型题型
字符串处理很高压缩解压、去重排序、子串匹配
数组 / 双指针很高滑动窗口、快慢指针、区间合并
排序 / 贪心高任务调度、区间问题、最优化分配
动态规划高背包、最长子序列、编辑距离
流程模拟中高状态机、指令解析、时间线模拟
DFS / BFS中岛屿数量、迷宫最短路径
图论低最短路、拓扑排序

为什么是这几个方向?因为机试不是奥赛,它考察的是工作里真正会用到的基础编码能力。字符串处理贴近日常开发,双指针和滑动窗口是基础算法思维,动态规划考察的是状态设计能力,流程模拟则完全在模拟真实业务里那种"按照规则一步步推进"的场景。华为机试很少出偏难怪题,把常规题型的套路练透,比钻研冷门算法有用得多。

这套模拟题7的安排就很典型:第一题字符串解压缩、第二题最长无重复子串、第三题最长递增子序列、第四题任务调度。四道题对应四个方向,难度逐步爬升,基本就是机试出题的常规配方。

2. 模拟题7核心真题思路拆解

2.1 第一题·字符串解压缩:套路题里的细节陷阱

2.1.1 题目描述与题意分析

题目给一个经过压缩的字符串,比如3[a2[bc]],要求还原成abcbcabcbcabcbc。规则是数字[字符串]表示中括号里的内容重复数字次,中括号支持嵌套。输入只包含数字、小写字母和方括号,输出解压后的完整字符串。

这题第一眼看过去就是典型的栈应用场景。为什么想到栈?因为嵌套结构天然就是"后进先出"——最内层的中括号要先处理完,再把结果交给外层。这种结构用递归也能解,但栈更直观,而且不用担心递归深度问题。机试环境里,能用迭代替代递归的就别用递归,减少不可控因素。

2.1.2 代码实现与关键细节

我写的是 Python 版本:

def decode(s: str) -> str: stack = [] # 栈中元素为 (之前的字符串, 重复次数) cur = "" # 当前累计的字符串 num = 0 # 当前累计的数字 for ch in s: if ch.isdigit(): num = num * 10 + int(ch) elif ch == '[': stack.append((cur, num)) cur = "" num = 0 elif ch == ']': prev, repeat = stack.pop() cur = prev + cur * repeat else: cur += ch return cur s = input().strip() print(decode(s))

这里有个特别容易踩的坑:数字可能是多位数。比如12[a],如果你一看到数字就直接处理,不累加,很可能只重复 2 次而不是 12 次。所以代码里用了num = num * 10 + int(ch)做累加,这是处理多位数数字的标准写法。

另一个坑是]出栈后的拼接顺序。pop()出来的是[之前已经拼好的字符串,也就是示例里的外层前缀,所以必须写成prev + cur * repeat,不能反过来。我在第一次写的时候就栽在这了,输出变成了bcbcbcaaaaaa这种奇怪的结果。这类题目没什么高深算法,但细节特别多,平时写的时候就要养成"边写边在脑子里模拟一个简单用例"的习惯。

2.2 第二题·最长无重复子串:滑动窗口的边界功夫

2.2.1 题目描述与双指针思路

题目很经典:给定一个字符串,比如abcabcbb,找出其中不含有重复字符的最长子串的长度,答案是 3,对应子串abc。这题在 LeetCode 上是第 3 题,机试也经常以各种变体出现。

解法的核心思想是滑动窗口,用两个指针维护当前无重复区间。右指针负责扩展,遇到重复字符时,左指针跳到上一个相同字符的后面。很多人第一反应是用集合存当前窗口的字符,但那样需要配合循环去重;更优雅也更不容易出 bug 的做法是直接用哈希表记录每个字符最后出现的位置。

2.2.2 代码实现与易错点
def max_unique_len(s: str) -> int: last_pos = {} # 字符 -> 最后一次出现的下标 left = 0 ans = 0 for right, ch in enumerate(s): # 如果 ch 出现过,且出现位置在窗口内,左边界移动 if ch in last_pos and last_pos[ch] >= left: left = last_pos[ch] + 1 last_pos[ch] = right ans = max(ans, right - left + 1) return ans s = input().strip() print(max_unique_len(s))

最关键的判断是last_pos[ch] >= left,这个条件不能省。为啥?因为last_pos记录的是字符在整个字符串中最后一次出现的位置,但如果这个位置已经在左边界左边了,说明它不在当前窗口内,没必要为了它收缩窗口。少了这个判断,窗口可能不能正确收缩,答案也会出错。

我举一个实际例子:字符串abba。如果不用>= left这个条件,遍历到最后一个a时,last_pos['a']是 0,而当前left已经是 2,如果直接left = last_pos['a'] + 1,left 会回退到 1,窗口长度反而变长了。这个"回退"错误是这题最容易犯的,几乎每次讲解这题我都会单独拎出来强调。

2.3 第三题·最长递增子序列:动态规划的两种层次

2.3.1 题目描述与 DP 基础解法

题目给定一个整数数组,比如[10, 9, 2, 5, 3, 7, 101, 18],要求返回最长严格递增子序列的长度。这里"子序列"不要求连续,但元素的相对顺序不能变。答案是 4,对应[2, 3, 7, 101]或[2, 5, 7, 101]。

基础解法是动态规划:定义dp[i]表示以第i个元素结尾的最长递增子序列长度。状态转移需要遍历所有j < i,如果nums[j] < nums[i],就用dp[j] + 1更新dp[i]。初始值都是 1,因为每个元素自身就是一个长度为 1 的子序列。

def lis(nums): if not nums: return 0 n = len(nums) dp = [1] * n ans = 1 for i in range(n): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) ans = max(ans, dp[i]) return ans nums = list(map(int, input().split())) print(lis(nums))

这个解法 O(n^2),数据范围 10^3 以内随便跑,但如果 n 到了 10^5 就会被卡超时。机试题目一般会把数据范围写在题目描述里,做题前先扫一眼数据范围,这比什么都重要。

2.3.2 贪心 + 二分的进阶写法

如果要处理 10^5 级别的数据,就得用贪心加二分的思路。维护一个tails数组,其中tails[k]表示长度为k+1的递增子序列中末尾元素最小的那个值。遍历每个数,在tails里找第一个大于等于当前数的位置,替换掉;如果当前数比tails里所有元素都大,就追加到末尾。

import bisect def lis_greedy(nums): tails = [] for x in nums: pos = bisect.bisect_left(tails, x) if pos == len(tails): tails.append(x) else: tails[pos] = x return len(tails)

我最初学这个方法的时候一直有个困惑:tails数组替换来替换去,最后得到的数组并不是真正的递增子序列,为什么长度就对了?后来想明白了,tails本质上维护的不是一个真实存在的序列,而是"每个长度下的最小末尾值"这个信息。长度相同的子序列,末尾越小,越有可能在后面接上更大的数。这也是贪心思想的体现。

我当时做模拟题7的第三题,第一时间写的是 O(n^2) 的 DP,因为怕二分写错反而丢分。测了一下数据范围不大,就保留了。机试的原则是"够用就好",先保证正确性,再考虑优化,没必要为了炫技去写复杂解法。

2.4 第四题·任务调度模拟:流程题最考验全局观

2.4.1 题目描述与输入输出格式

这题是这样的:给定 N 个任务,每个任务包含到达时间、执行时长和优先级(数字越小优先级越高)。CPU 每个整数时间点从"已到达且未完成"的任务里选优先级最高的执行 1 个单位时间,优先级相同则先到达的先执行。要求输出每个任务的完成时间。

输入格式:

3 0 5 2 1 2 1 2 4 3

第一行是任务数 N,后面 N 行每行是任务的到达时间、执行时长、优先级。任务按输入顺序编号为 0、1、2。

输出格式:按编号输出每个任务的完成时间,用空格分隔。

2.4.2 优先队列模拟思路

模拟题的核心是找对"主循环"。这里最直观的主循环是时间步进,从 0 开始,每一秒做三件事:先把所有到达时间小于等于当前时间且未入堆的任务放入优先队列,然后从堆里弹出一个任务执行 1 个时间单位,如果任务没执行完就重新入堆,执行完了就记录完成时间。

优先队列(最小堆)存什么?这里有个细节:要让堆顶始终是"优先级最高"的任务。Python 的heapq默认是最小堆,所以直接存(优先级, 到达时间, 任务编号, 剩余时长),这样优先级数字小的先弹出,优先级相同就到达时间小的先弹出。

import heapq def schedule(n, tasks): tasks.sort() # 按到达时间排序 heap = [] idx = 0 time = 0 ans = [0] * n while idx < n or heap: # 堆为空但还有任务没到,直接把时间跳到下一个任务到达时刻 if not heap and idx < n and time < tasks[idx][0]: time = tasks[idx][0] while idx < n and tasks[idx][0] <= time: arrive, duration, priority, tid = tasks[idx] heapq.heappush(heap, (priority, arrive, tid, duration)) idx += 1 if heap: priority, arrive, tid, remain = heapq.heappop(heap) if remain - 1 > 0: heapq.heappush(heap, (priority, arrive, tid, remain - 1)) else: ans[tid] = time + 1 # 当前时间点执行完后完成 time += 1 return ans n = int(input()) tasks = [] for i in range(n): a, d, p = map(int, input().split()) tasks.append((a, d, p, i)) ans = schedule(n, tasks) print(" ".join(map(str, ans)))
2.4.3 这题容易错在哪

第一个坑是堆里比较元的顺序。如果不把到达时间放进去,优先级相同但先到达的任务可能排到后面,破坏题目条件的"先到先执行"。这种错误在本地测简单的两三个任务时可能看不出来,但只要用例里出现同优先级任务,就会挂掉。

第二个坑是时间跳跃。如果堆空了,但下一个任务到达时间还是 100,还傻乎乎一秒一秒加到 100,虽然结果对,但效率很低。代码里用了一个if not heap and idx < n and time < tasks[idx][0]的情况,直接把 time 跳到下一个到达时间,这是模拟题常用的优化技巧。

第三个坑是完成时间的计算。我用time + 1是因为任务在 "当前秒" 执行完,比如 time=0 时执行一个时长 1 的任务,它在第 1 秒结束,所以完成时间是 1。很多人会写成time或者time + 2,一测边界就露馅。写模拟题之前,先在草稿纸上把第一组简单的数据手动推一遍,确认你对"时间点"的定义,这个习惯能帮你省下不少调试时间。

3. 从读题到 AC 的一整套实战流程

3.1 拿到题目先别急着敲代码

我自己踩过最大的坑就是读题太急。前几次模拟考,题目看个大概就开始写,写到一半发现输入格式理解错了,推倒重来,时间全浪费了。后来总结了一套流程,现在每次做机试题都这么执行。

先读三遍。第一遍泛读,理解题目在说什么,输入是什么,输出是什么。第二遍精读,把输入输出的边界条件划出来,比如"整数范围""字符串长度""是否可能为空"。第三遍是拿题目给的样例手动推一遍,确认你对规则的理解和样例输出能对上。这个过程看着慢,实际上能防止后期返工,是最省时间的做法。

然后是写"注释式伪代码"。不要直接写实现,先在代码注释里写出整体框架:读入数据 -> 核心处理 -> 输出结果。等框架清楚了,再往里面填逻辑。像任务调度那题,我就是先写了:

# 1. 读入所有任务 # 2. 按到达时间排序 # 3. 时间从0开始,每步:把到达任务入堆 -> 执行一个任务 -> 记录完成时间 # 4. 输出结果

框架定了,后面的实现就是按部就班。头脑清醒的状态下写代码,出错率能低一半。

3.2 调试、边界测试和性能优化

代码写完先别急着提交,花两分钟做三件事。第一,用题目给的样例跑一遍,确认输出一致。第二,自己构造几个边界用例:空输入、最小输入、全是重复值、数值极大。第三,检查输出格式,结尾有没有多余空格、有没有缺失换行。这些细节虽然不涉及算法,但扣的分跟算法错误一样多。

调试时最常用的还是打印中间变量。比如字符串解压缩那题,如果输出不对,就在每个]处理的地方打印一下prev、repeat和cur,基本一眼就能看出是拼接顺序问题还是数字累加问题。机试环境一般不支持断点调试,所以在代码里临时加print是最直接的排查手段,排完记得删掉。

关于性能优化,有一个原则:先暴力,再优化。第一版不管是 O(n^2) 还是 O(n^3),只要能跑出正确结果,就先把暴力写出来保底。然后再根据数据范围决定要不要优化。如果 n 是 100,O(n^3) 也没问题;如果 n 是 10^5,O(n^2) 就可能超时。机试的时间限制通常比较紧,所以我在写完暴力版本后一定会看一眼数据范围,评估是否需要用更优解法替换。

4. 不同基础怎么安排刷题路线

4.1 按基础水平的三条备考路线

机试备考最忌讳的就是"一刀切"式刷题。我见过基础很弱的人上来就死磕困难动态规划,也见过刷了几百题的人还在反复做简单字符串题,这两种都是时间浪费。我把备考的人粗分为三类,对应不同的侧重点。

基础较弱的,先别着急刷题,花两三天把语言基础补到位,重点练字符串处理、列表/数组操作、常见数据结构的增删查改。然后每天做两到三道简单题,刷题时要求自己不看题解独立完成。这个阶段的目标不是刷多少题,而是建立"读题 -> 写代码 -> 调试通过"的闭环信心。考试时目标要明确:保住前两题,第三题写暴力拿部分分。

有一定基础、刷过 100 到 200 题的,重点是按专题突破。一周里安排字符串、双指针、动态规划、模拟四个专题,每个专题集中刷两天。这个阶段最容易陷入的误区是"舒适区刷题",简单题刷得飞起,一碰到动态规划就跳过。一定要逼自己每天至少做一道不会的题,哪怕想不出来,看了解析后要能独立复现。目标就是前三题稳定 AC,第四题争取相当部分用例。

基础扎实、刷题 300 以上的,重心要放在"机试实战化"上。这个时候纯题目已经不太能提高能力了,关键是把平时刷题的方式切换到限时模式:每周至少两次完整模拟,严格按照机试的时长和单题时间分配来练习。重点训练的是在时间压力下怎么选择策略、怎么快速排查边界条件、怎么写代码一遍过。目标就是全卷稳定高分。

4.2 刷题优先级与每日时间分配

根据考点频率,我建议按这样的优先级排序备考:字符串处理、双指针/滑动窗口、排序/贪心、动态规划、流程模拟、DFS/BFS。前四类是必争之地,后面的看时间分配。机试的高频题大多集中在这些方向,把它们练熟,效果比盲目刷 500 道冷门题好得多。

时间分配上,如果只剩一周,重心放在简单题的熟练度和字符串处理上,每天一套限时模拟,模拟完立刻复盘,把卡壳的题整理到错题本。如果有两周到一个月,第一周按专题刷中档题,第二周开始每周两到三次限时模拟。如果有一个月以上,可以系统做专题刷题,每天保持一小时量,周末完整模拟一套。

错题本这个东西,我建议只记原因,不抄题目。比如记录"这题卡住是因为没考虑负数输入""模拟题超时是因为循环里重复扫描数组"这类结论,而不是把整道题抄一遍。考前过一遍错题本,比临时翻阅题单有用得多。

5. 机试避坑指南:这些细节决定成败

5.1 高频踩坑点速查表

我把机试里常见的问题整理成一张表,这些都是实际操作中反复出现的坑:

问题类型具体表现解决办法
输入读取题目是多组输入,只读了一组先用样例测一下,观察是否有 EOF 标志
输入读取输入数字跨多行,input().split()只拿到一部分用sys.stdin.read().split()统一读入再解析
输出格式行尾多了一个空格最后一个元素单独打印,或者用" ".join()拼接
输出格式大小写不匹配按题目要求原样输出,别自己"修正"
递归Python 递归深度不够能用迭代就别递归,必要时sys.setrecursionlimit(1000000)
性能O(n^2) 在大数据下超时先看数据范围,提前想好优化方案
性能Python 字符串频繁拼接很慢结果用列表收集,最后"".join()
数组越界访问下标为 -1 的元素没意识到涉及索引运算时用边界值手动推一遍
初始化数组默认值没设置对特别注意dp数组初始值是否应该是 1 而不是 0

第 5 行那个多行输入的问题我单独解释一下。机试的输入有时不给明确的行数,数据可能换行也可能不换行,如果用input().split()就会漏读。更稳妥的写法是:

import sys data = list(map(int, sys.stdin.read().split()))

这样不管数据切成几行,都能一次性全部读进来。我后来做模拟题7的时候,只要是"读一堆整数但不确定行数"的题,默认就用这种写法,省心很多。

5.2 考场战术与心态

机试的节奏和平时练习完全不同。我的经验是拿到题目后先全部扫一遍,10 秒内判断每道题的难度,然后从最简单的开始做。先保证把简单题的满分拿到手,再去啃难题,不要在第一题上卡 40 分钟,导致后面两道题连读题时间都没有。

还有一个很现实的策略:对每道题设置一个时间上限。比如简单题最多 20 分钟,中档题最多 30 分钟,超过上限就立刻切换到"保底模式"——把暴力解法写上,用例能过多少算多少。这样做的原因是机试按用例比例给分,与其在一个难题上耗到最后一无所有,不如保证已经拿到的分不丢。

心态方面,机试的限时环境很容易放大人的焦虑感,尤其是前面一道题不顺的时候。我自己的调节方法是:卡住 10 分钟就想"这题最多 30 分,后面还有 70 分",该跳就跳,别让一道题毁掉整场状态。做过几次限时模拟之后,你会发现紧张感会明显降低,熟练度是缓解焦虑的最好方式。

最后再分享一个小技巧

做模拟题7的过程中我收获最大的一件事,是养成了"每题写完都手动跑一遍边界用例"的习惯。以前总觉得自己思路对就完事了,结果每次出问题的都是那些看似正常的边界条件,比如空字符串、长度为 1 的数组、全是相同字符的串。现在写完后我会盯着代码想:如果输入是极端情况,我的代码会走哪条分支?有没有可能越界?这个习惯在机试里救了我好几次。

如果你现在刚开始准备机试,我的建议很直接:找一套模拟题,限定时间完整做一遍,然后根据暴露出来的问题调整复习方向。做过一套之后,你对自己的短板就心里有数了,比自己闷头刷几十道题都管用。

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

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

立即咨询