尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

华为OD机试备考:模拟题6覆盖字符串、调度与BFS高频考点

华为OD机试备考:模拟题6覆盖字符串、调度与BFS高频考点 华为OD机试的通过率为什么一直上不去我陪跑了好几批准备机考的朋友之后发现大部分人根本不是挂在算法难度上而是挂在备考方式上。很多人上来就闷头刷LeetCode刷了几百道却连机考环境长什么样都不清楚也有人对着真题题库一份份硬背结果题目稍微换个叙事背景就懵了。这篇文章要拆解的是一套典型的华为机试编程模拟题6覆盖字符串处理、哈希计数、贪心调度、优先队列、BFS图搜索这几个高频考点适合正在冲刺华为OD机考C卷、或者想系统练手大厂算法题的开发者参考。认真过一遍这套题基本等于把大厂机试最常考的几类题型完整走了一遍。1. 华为机试的底层逻辑与备考思路1.1 题型结构与分值分布华为OD机试的C卷在我接触到的信息里一般是一套三道编程题考试时长150分钟总分400分。分数分布大概是第一题100分、第二题100分、第三题200分按难度梯度排列。这跟LeetCode那种每题随机难度、核心代码模式的刷题体验完全不同它是ACM模式你不仅要写算法逻辑还得自己写输入解析、自己构造输出格式甚至要处理多行数据、空行、特殊字符这些边界情况。很多第一次参加机考的人最容易低估的就是这个ACM模式。你平时在力扣上写个function就完事了但机考系统里你提交的是一份完整可运行的程序——没有现成的函数签名给你没有帮你处理好的测试用例读取一行数字都得自己写。我见过好几个代码能力其实不差的朋友第一次模拟考试时卡在读取输入上白白浪费了二十分钟。另外要明白一个关键点机考不要求你拿满分。OD机试的通过线一般是150分也就是说总分400分里拿到150分就有面试资格。这意味着合理策略是第一题保稳、第二题争取、第三题尽力。很多攻略只讲难题怎么解忽略了一个事实——真正拉开差距的往往是第一题和第二题的稳定拿分率。你第三题写个大暴力骗个几十分前面两题稳稳拿下过线很轻松反过来前面两题各卡半小时第三题再简单也会因为时间不够而崩盘。1.2 模拟题6的三个核心考点模拟题6这套卷子我选的时候刻意让它覆盖三个方向一是字符串加哈希排序这类题几乎是机试必考不管怎么换题干本质都是统计频次多关键字排序二是调度类问题核心是贪心思想加优先队列这在100分题里出现概率极高三是图上搜索也就是BFS/DFS一般落在200分的压轴题。这三个方向基本涵盖了高校计算机基础课里数据结构与算法最核心的部分也是实际工程里最常用到的能力。选这个组合还有一个原因——它们的解题套路相对固定非常适合在短时间内通过模拟训练形成肌肉记忆。字符串拆分、哈希计数、排序的lambda写法、优先队列的贪心模拟、BFS的队列状态搜索这些都是熟练之后能快速写出来的固定套路。你需要做到的不是会做而是拿到题30秒内能判断出题型、套上对应框架。2. 第一题单词频次统计与输出排序100分2.1 题目描述与输入输出约定先看题目。给定一行英文句子单词由大小写字母组成单词之间用空格分隔句子中可能混有逗号、句号、感叹号等标点符号标点紧跟在单词后面。现在需要统计每个单词出现的次数不区分大小写然后输出出现次数最多的前K个单词。排序规则是先按出现次数从高到低出现次数相同按单词字典序从小到大。输入格式是这样第一行一个整数K1 ≤ K ≤ 10 第二行一个字符串S长度不超过10000仅包含字母、空格和常见标点,.!?输出要求输出前K个单词及出现次数每行一个单词和次数用空格隔开。如果单词种类数不足K个则按实际种类输出全部。我实际用它来考察候选人时示例数据是这样的输入 2 Hello world. hello World! 输出 hello 2 world 2这道题表面看是让统计单词但真实考点有三个字符串切分的健壮性、大小写归一化、多关键字排序。其中第一个是很多人翻车的地方因为标点符号紧跟在单词后面用简单的split( )得到的可能是world.这种带点尾巴的脏数据。2.2 解题思路与代码实现我的做法是先想清楚什么是单词连续的一段字母字符。这个定义一出来答案就清晰了——用正则表达式提取所有字母连续段是最省力、最不容易出错的方式。当然你不用正则也能写用双指针逐个字符扫描也行但正则[A-Za-z]一句就把问题解决了代价只是多引入一个re模块。执行流程分四步读出K和第二行原始文本用re.findall提取所有纯字母单词并统一转成小写用Counter统计频次按(-次数, 字典序)排序截取前K个逐行输出。这里排序的写法很关键Python里用sorted(cnt.items(), keylambda x: (-x[1], x[0]))利用元组当作复合排序键第一个值取负数实现降序第二个值保持升序。这算是个小技巧如果你先按次数排、再按字典序排分两步写还容易出错一次性写好最稳。完整可运行的代码是这样的import sys import re from collections import Counter def main(): data sys.stdin.read().splitlines() if not data: return k int(data[0].strip()) text .join(data[1:]) # 防止句子被莫名其妙地拆成多行 words re.findall(r[A-Za-z], text) words [w.lower() for w in words] cnt Counter(words) top sorted(cnt.items(), keylambda x: (-x[1], x[0]))[:k] for word, count in top: print(f{word} {count}) if __name__ __main__: main()2.3 避坑点拆分与排序的细节这道题容易踩的坑有三个。第一个坑是标点处理。用split( )的写法遇到world.就完蛋world.会被当成一个独立单词。正则提取直接绕开了这个问题。如果你不想用正则那就必须自己写扫描逻辑从头到尾遍历遇到字母就累积遇到非字母就把累积的单词写入结果然后清空。这个写法也不算复杂但代码量明显更多。第二个坑是大写归一化时机。统计之前必须lower()否则Hello和hello会被当成两个词。这个其实只要读题仔细就不会错但考试紧张时真有人会漏掉这一步。我通常建议把转小写这步放在提取单词之后立刻做写成链式调用减少记忆负担。第三个坑是K值大于单词种类数。切片[:k]天然支持不够就全给的行为这个不用担心。但如果你手写循环去取前K个就要写条件判断否则可能数组越界。第四个坑相对隐蔽第二行可能包含大量空格甚至可能被系统读取时切开。所以我用sys.stdin.read().splitlines()一次读完全部内容然后把第二行及之后的行用空格拼接这样即使句子中间有换行也不至于丢数据。真机考中输入格式是固定的但养成这个习惯能少掉很多脑细胞。3. 第二题最短作业优先的服务器任务调度100分3.1 题目描述与样例分析第二题是一个调度模拟题本质是经典的最短作业优先算法SJF。题目背景大概是这样的某服务器有一个任务队列任务按提交时间依次到达。每个任务有两个属性到达时间arrive[i]和执行耗时cost[i]。服务器同一时刻只能执行一个任务任务一旦开始执行就不能被打断也就是非抢占式。服务器每次从当前已到达但尚未执行的任务中选择执行耗时最短的任务如果有多个耗时相同的任务选择到达时间更早的那个。现在要求所有任务从到达时刻到开始执行的平均等待时间保留两位小数。这是一个典型的规则驱动型模拟题。它不考你高深的数学推导而是考你能不能把题目描述的调度规则转换成代码逻辑。这类题在100分档出现频率非常高因为它能同时考察排序、堆、循环控制和边界处理能力。我设计的输入样例是3 0 3 1 2 2 1三个任务任务0在时刻0到达耗时3任务1在时刻1到达耗时2任务2在时刻2到达耗时1。你的CPU从时刻0开始工作。时刻0只有任务0已到达所以它从0开始执行等待时间为0。任务0执行到时刻3结束。此时任务1和任务2都已经到达按规则选耗时最短的任务2执行任务2的开始时间是时刻3等待时间为3-21。任务2耗时1执行完到时刻4。最后执行任务1开始时间4等待时间4-13。总等待时间0134平均等待时间4/31.33。3.2 贪心模拟时间推进与优先队列这道题的核心逻辑是当前时刻的推进。我刚开始做这类题时很容易写出一个错误的版本把所有任务按到达时间排序然后从第一个任务开始一个一个执行。这是纯粹的FCFS先来先服务完全忽略了每次选择耗时最短这个关键规则。正确的模拟思路是维护两个数据结构一个按到达时间排序的原始任务数组一个小根堆存储当前已到达但未执行的任务。堆里的元素排序键是(cost, arrive)这样Python的heapq在弹出时就会自动先比较costcost相同再比较arrive。循环推进的逻辑是这样的如果堆为空说明当前时刻没有待执行任务但原任务列表里可能还有任务没到达。此时直接把当前时刻跳到下一个任务的到达时间。把所有到达时间≤当前时刻的任务全部入堆。从堆顶取出一个任务执行累加它的等待时间当前时刻减去该任务的到达时间。当前时刻增加该任务的耗时。重复执行直到所有任务都被处理完。这里最容易被忽略的是第一步的时间跳变。如果当前时刻是10下一个任务到达时间是15堆又为空你不能干等着必须把当前时间直接跳到15。这个跳变的正确性依赖于所有任务的到达时间都是非负整数且任务之间的空隙不可能产生更多任务。想明白这一点代码就不会出现死循环。3.3 关键代码与复杂度分析完整实现如下import sys import heapq def main(): data sys.stdin.read().split() if not data: return n int(data[0]) tasks [] idx 1 for i in range(n): arrive int(data[idx]) cost int(data[idx 1]) idx 2 tasks.append((arrive, cost)) # 按到达时间升序排列 tasks.sort() heap [] cur 0 # 当前时刻 total_wait 0 # 累计等待时间 i 0 # 已入堆的任务下标 while i n or heap: # 堆空但还有任务未到达时间跳变 if not heap and cur tasks[i][0]: cur tasks[i][0] # 到达时间 当前时刻的任务全部进入堆 while i n and tasks[i][0] cur: arrive, cost tasks[i] heapq.heappush(heap, (cost, arrive)) i 1 cost, arrive heapq.heappop(heap) total_wait cur - arrive cur cost print(f{total_wait / n:.2f}) if __name__ __main__: main()复杂度上每个任务入堆一次、出堆一次单次堆操作是O(log n)整体是O(n log n)。排序也占O(n log n)所以总复杂度O(n log n)在100分题里这个效率绰绰有余。如果你直接每次遍历找最小耗时任务那是O(n²)数据量一大就可能在评测机上超时。机考评测通常会跑比较大的测试数据不要抱有侥幸心理。还有一个实操层面的提醒这道题用Python写堆元组的顺序至关重要。heapq默认按元组第一个元素排序所以我把cost放在第一位arrive放第二位这样自动满足题目耗时相同按到达时间更早的规则。如果你反着放就要在堆里塞(arrive, cost)然后自己写比较逻辑完全没必要。这种用元组顺序天然实现排序规则的思路在机考里可以帮你省下大量时间。4. 第三题带传送门的迷宫最短路径200分4.1 题目描述与难点解析第三题进入200分档难度明显上来了。题目背景是迷宫寻路但加了传送门。给定一个N行M列的网格格子有几种取值S表示起点E表示终点.表示空地#表示障碍物数字0到9表示传送门。同一种数字在迷宫中恰好出现两次玩家走到其中一个传送门格子时会被立即传送到另一个相同编号的传送门格子并且传送本身不额外增加步数。玩家每一步可以向上下左右四个方向移动一格不能走出网格也不能走进障碍物。问从起点到终点最少需要多少步如果无法到达输出-1。这类最短路径题型一看就是在考查BFS。BFS天然按层次遍历第一次到达某个格子时的步数就是最短步数这是图论里最基础也最实用的结论。难点不在BFS本身而在传送门的处理传送门的落点本身可能也是一个传送门如何避免死循环传送后是否需要重新计算步数到达传送门的那一步和传送到落点的那一步算几步题目里传送不额外增加步数这个条件很关键——它意味着你从普通格子走到传送门格子付出了1步然后瞬间被扔到另一个传送门格子步数仍然是那1步不会因为传送而增加。换句话说传送是跟在移动后面的一次免费操作。4.2 BFS状态搜索与传送门处理我的处理思路是在BFS扩展邻居节点时判断邻居是否为传送门。如果是传送门就找到同编号的另一个位置把实际落点替换成另一个传送门的位置。然后判断实际落点是否已经访问过如果没访问过就标记并入队。这里有一个隐藏细节传送到另一个传送门后要不要再检查落点是否传送按题目规则一次移动只触发一次传送所以我不会在传送后再递归判断。这种做法很关键否则A门 - B门 - A门会无限循环下去。实际代码里因为我是在遍历邻居时只做一次传送修正所以天然避免了二次触发。传送门的预处理也要注意。我用一个字典键是数字字符值是该数字对应的两个位置列表portals {} for i in range(n): for j in range(m): if grid[i][j].isdigit(): portals.setdefault(grid[i][j], []).append((i, j))这样在BFS中遇到某个数字ch时遍历portals[ch]找到不是当前格子(nx, ny)的那个位置就得到了传送落点。4.3 代码实现与边界条件完整代码如下import sys from collections import deque def main(): data sys.stdin.read().splitlines() if not data: return n, m map(int, data[0].split()) grid [] for i in range(1, n 1): grid.append(list(data[i].strip())) portals {} start end None for i in range(n): for j in range(m): ch grid[i][j] if ch S: start (i, j) elif ch E: end (i, j) elif ch.isdigit(): portals.setdefault(ch, []).append((i, j)) dist [[-1] * m for _ in range(n)] dist[start[0]][start[1]] 0 q deque([start]) dirs [(-1, 0), (1, 0), (0, -1), (0, 1)] while q: x, y q.popleft() if (x, y) end: print(dist[x][y]) return for dx, dy in dirs: nx, ny x dx, y dy # 越界或障碍物 if nx 0 or nx n or ny 0 or ny m: continue if grid[nx][ny] #: continue # 传送门处理找到同编号的另一个位置 if grid[nx][ny].isdigit(): ch grid[nx][ny] for px, py in portals[ch]: if (px, py) ! (nx, ny): nx, ny px, py break # 传送后可能又越界不会因为传送点本身在网格内 # 但要检查传送后的位置是否已访问 if dist[nx][ny] ! -1: continue dist[nx][ny] dist[x][y] 1 q.append((nx, ny)) print(-1) if __name__ __main__: main()边界条件有几个值得单独拿出来说起点和终点不会是传送门这是我出题时保证的省去了处理起点即传送门的复杂度。如果你实战中遇到起点本身就带传送效果的题需要在初始化时就做一次传送处理。传送后落在障碍物的可能性在我这个设定里不存在因为每个数字的两个位置都是传送门格子本身就不是障碍物。但如果题目改成传送门站在空地上那就得在传送修正后再做一次障碍物判断。你在实际写代码时建议把越界和障碍物判断都放在传送处理之后这样逻辑更安全。关于标记数组我用的是dist矩阵存步数初始值为-1表示未访问。这里有个容易犯的错当传送门把位置从A修正到B后直接对(nx, ny)做判断和执行如果这个位置恰好已经被访问过就跳过。这样避免了重复入队也避免死循环。这道题如果换成DFS来做大概率会超时因为DFS要找最短路径需要遍历所有可能路径复杂度指数级。BFS是唯一的正解除非你在DFS里做剪枝优化但机考环境下没必要给自己找这个麻烦。5. 新系统双机位考试环境与上分策略5.1 双机位考试流程与设备要求现在华为OD机考用的是新系统双机位模式这个变化影响很大。第一机位是电脑自带的摄像头需要正对着你的脸第二机位是手机需要你用微信或指定App扫屏幕上的二维码然后把手机放在你的斜后方45度左右的位置确保摄像头能同时拍到你的电脑屏幕和手部操作。考试开始前会有一次设备检测和模拟测试这个时候一定要耐心把流程走完别嫌麻烦。我见过有人手机角度不对考试中途被监考系统判定监控画面异常影响心态。浏览器只能用Chrome或Edge别用系统自带的IE、360之类的。考试页面会锁定全屏一旦你把鼠标移到屏幕边缘试图切出去系统就会记录切屏次数。切屏超过一定次数哪怕你最后交卷了成绩也可能作废。这个不是说笑的我陪跑时有个人就是不小心弹了个广告窗口被记了一次后面全程战战兢兢。所以考试前把所有无关软件关掉消息通知关掉最好开个免打扰模式。还有个细节是代码编辑器。新系统是在网页里内嵌的代码编辑器支持语法高亮和自动缩进但补全功能很弱也没有本地IDE那种调试器。这意味着你必须在脑子里把逻辑理清楚再写不能靠IDE帮你纠错。平时练习时我建议你也用网页版编辑器写或者至少把IDE的自动补全关掉提前适应这种裸写状态。5.2 上分策略与时间分配150分钟的考试时间我的建议分配是第一题控制在25分钟以内第二题控制在35分钟以内剩余时间全部给第三题。第三题如果读完题十分钟内没思路不要死磕先把最暴力的解法写出来拿到部分分数再说。200分题的测试数据通常是大数据暴力解能过前几个小数据拿个三四十分不亏。还有一个我反复强调的原则先保证代码能编译通过、样例能跑对再考虑优化。很多人喜欢在写题过程中反复调整算法结果最后连完整代码都没提交上去。机考本质是按测试用例给分一个样例都过不了就是0分能过部分样例就有部分分两者天差地别。代码风格上变量命名别太随意。虽然机考只看结果但你自己的大脑也需要维护这些代码。我通常用arrive、cost、dist这种有意义的命名写起来慢不了多少调起错来快很多。另外提交之前一定要重新读一遍题干。我记得有一次模拟考试题目要求的输出格式是结果保留两位小数我样例跑出来是整数想都没想就提交了结果全错。这类格式细节掉进坑里的人太多了宁可多花一分钟检查也不要浪费一整个提交机会。6. 实战中容易踩的坑与调试经验6.1 输入输出与ACM模式我把平时带人做模拟题时的常见问题整理了一下排在第一位的就是输入输出。华为机试用的是ACM模式不会有人帮你把输入切好放到参数里你必须自己处理。最常见的坑是用input()逐行读但遇到多行输入时读漏或者用sys.stdin.read()一次读完结果把第一行的数字也拼进文本里了。我自己的习惯是如果输入是一行一行的结构化数据用sys.stdin.read().split()转成token列表再按顺序取。文本类题目比如第一题的句子用sys.stdin.read().splitlines()。读完后先打印出来自测一遍确保数据切分正确再开始写核心逻辑。这个自测步骤虽然不起眼但能避免大量低级错误。还有输出精度。要求保留两位小数就用f{value:.2f}要求四舍五入就用round()。不要混用round(2.675, 2)在Python里会得到2.67因为浮点数精度问题这类面试题经常拿来挖坑。6.2 高频Bug与调试技巧第二类常见问题是排序和堆的键值顺序。机考环境没有IDE提示你很容易把keylambda x: (-x[1], x[0])写成(x[1], -x[0])把降序升序搞反。我的排查技巧是先打印排序前后的结果肉眼对比一下是否符合规则再继续往下写。优先队列的元组顺序同理不确定就打印堆的内容看一眼别凭感觉硬写。第三类是BFS/DFS的边界判断。越界判断必须放在访问grid之前Python里grid[nx][ny]在nx越界时会直接抛IndexError。正确写法是先判断if nx 0 or nx n or ny 0 or ny m: continue再判断格子内容。这个顺序我见过太多次反着写导致Runtime Error的案例了。第四类是超时。O(n²)的算法在30000级别数据量下会非常吃力Python尤其明显。如果你写完发现运行超时先看能不能引入哈希表把查找从O(n)变成O(1)或者用堆把排序查找变成O(log n)。绝大多数机考题的优化方向就这两个。我再分享一个通用的调试技巧准备一份小数据集的暴力解法和你的优化解法同时跑随机生成输入对比输出。这个对拍思路在竞赛里很常用机考备考同样适用。它能帮你快速定位逻辑错误而不是对着一个错误的输出干瞪眼。Python写暴力解很快花十分钟写个验证脚本能省半小时的排查时间。最后再分享一个小技巧刷题不在多而在精。模拟题6这套卷子做完你不需要急着找下一套先把这三道题的解法吃透然后做一件事把每道题的代码重写一遍不看答案默写。默写不出来就回到博文里看看完再默写。我陪跑的人里凡是能把这个流程走完的机考基本都过了。如果你时间紧张至少把第一题和第二题的满分思路吃透第三题掌握BFS的套路框架。至于更多题型的扩展比如动态规划、贪心与二分结合、并查集这类可以在过了150分线之后再慢慢补。稳扎稳打比你焦虑地刷一百道题有效得多。
返回列表