
1. 项目概述从一道真题看华为OD机试的实战导向最近在帮几个准备华为OD机试的朋友做模拟练习发现大家普遍对“快递投放问题”这类题目感到棘手。这题在各大论坛和备考群里的讨论热度一直很高因为它不像纯算法题那样有固定的“套路”而是更贴近实际业务场景考察的是综合问题建模和工程实现能力。简单来说题目会给你一份快递的寄送清单包含快递ID、出发城市、目的城市再给你一份道路信息表包含城市A、城市B、道路是否开放最后还会有一份禁运规则比如某个快递不能经过某个城市。你需要计算在遵守所有规则的前提下所有快递能否成功送达并找出那些无法送达的快递。这听起来是不是很像一个简化版的物流调度系统没错华为OD的机试真题往往就是这样它不满足于考察你会不会写快排、会不会用DFS它更想知道你能否把一个模糊的业务需求转化成一个清晰的、可计算、可实现的程序模型。这道题的价值在于它完美地模拟了软件开发中“需求分析-抽象建模-算法设计-代码实现”的全过程。对于正在准备机试的开发者尤其是希望从传统业务开发转向大厂核心研发岗位的朋友来说吃透这类题目其意义远超过刷十道LeetCode上的“孤岛问题”或“接雨水”。它考察的维度更立体你的数据结构设计是否合理用邻接表还是邻接矩阵存图你的搜索策略是否高效BFS还是DFS需不需要剪枝你的边界条件处理是否周全没有路径怎么办起点就是禁运城市怎么办以及最终你的代码是否清晰、健壮、易于维护。接下来我就结合自己当年备考和后来担任面试官辅助评阅的经验把这道题的“里子”和“面子”都拆开揉碎了讲清楚并提供C、Java、Python三种主流语言的实现参考希望能帮你打通任督二脉。2. 核心需求解析与问题建模面对“快递投放问题”第一步也是最关键的一步不是急着写代码而是彻底理解题目并完成问题抽象。很多同学栽跟头就是因为没把题目中的“业务语言”准确翻译成“计算机语言”。2.1 题目要素拆解通常题目输入会包含三个核心部分快递列表每个快递有唯一ID、起始城市src、目的城市dst。这是我们需要处理的任务实体。道路网络描述城市之间的连通性。形式可能是(cityA, cityB, status)status表示道路状态如1开放/0封闭。这定义了快递可以行走的“图”。禁运规则描述特定快递在特定城市的限制。形式可能是(package_id, forbidden_city)。这是搜索路径时必须遵守的约束。输出要求一般是列出所有无法成功送达的快递ID。送达成功的标准是存在一条从src到dst的路径且路径上的所有道路状态均为“开放”同时路径不经过该快递的任何禁运城市。2.2 抽象为图论问题经过拆解我们可以清晰地将其映射到一个经典的图论模型顶点每个城市是一个顶点。边每条开放的道路status为开放是连接两个顶点的一条无向边通常快递运输可视为无向。任务对于每个快递任务在图中寻找从起点src到终点dst的一条路径。约束寻找路径时需要动态排除该快递特定的禁运城市顶点。因此这本质上是一个带顶点访问限制的图连通性/路径搜索问题。对于每个快递我们只需要判断是否存在一条符合约束的路径而不需要找出具体的最短路径除非题目特别要求。这决定了我们的算法选择可以更灵活。2.3 算法选型思考为什么选择BFS或DFS而不是更复杂的Dijkstra需求定位本题核心是“是否存在”而非“最短距离”。在边权一致可视为1且只关心连通性的场景下BFS/DFS的时间复杂度是O(VE)比Dijkstra的O((VE)logV)更优。BFS vs DFS两者均可。BFS能天然找到最短路径按边数计虽然本题不要求但其层序扩张的思路清晰。DFS实现更简洁递归或栈都容易编写。在顶点数不多的情况下性能差异不大。个人更推荐BFS因为它的迭代特性更容易处理路径记录和禁运判断且不会因图深度过大导致递归栈溢出。剪枝的重要性这是优化的关键。一旦发现当前路径包含了禁运城市这条搜索分支就应该立即终止。在BFS中可以在将下一个城市加入队列前进行判断。注意务必仔细阅读输入格式。有些题目变体可能要求输出具体路径或道路是单向的有向图或者禁运规则是“快递对城市对”的形式。准确理解输入输出规范是正确建模的前提。3. 数据结构设计与核心实现思路思路清晰后接下来就要设计程序的数据骨架和算法流程。好的设计能让代码写起来事半功倍。3.1 数据结构选择我们需要高效地存储和查询图结构、快递信息以及禁运规则。图的存储推荐使用邻接表特别是当城市数量较多但道路相对稀疏时现实中的交通网正是如此邻接表比邻接矩阵更节省空间遍历邻居也更高效。C可以用unordered_mapstring, vectorstring graph键是城市名值是该城市直接相连的所有城市列表。Java使用HashMapString, ListString graph。Python使用defaultdict(list)最为方便。禁运规则存储需要快速判断“快递P是否禁止经过城市C”。最佳结构是嵌套哈希集合。Cunordered_mapstring, unordered_setstring restrictions。键是快递ID值是该快递禁运的城市集合。JavaHashMapString, SetString restrictions。Pythondefaultdict(set)。这样判断操作restrictions[pid].count(city)的时间复杂度是O(1)。快递列表存储用一个列表或数组存储所有快递对象即可每个对象包含id, src, dst。3.2 核心算法流程以BFS为例整个程序的执行流程可以概括为以下几步我将结合流程图这里用文字描述和关键代码段来说明第一步数据读取与存储解析输入字符串填充packages列表、graph邻接表只添加状态为“开放”的道路、restrictions禁运字典。第二步逐个快递处理遍历packages列表对每个快递执行路径检查。第三步单快递BFS路径检查这是最核心的函数canDeliver(package_id, src, dst)初始化创建队列queue放入起点src。创建已访问集合visited加入src。创建可选路径记录parent字典用于回溯如果题目要求输出路径。获取禁运集从restrictions中取出该快递的禁运城市集合forbidden。BFS循环弹出队首城市current。如果current dst说明找到路径返回true。遍历graph[current]中的所有邻居城市next剪枝判断1如果next在visited中跳过。剪枝判断2如果next在该快递的forbidden集合中跳过此路不通。通过判断则将next标记为已访问加入队列并记录父节点。队列清空如果BFS结束仍未找到dst说明不存在可行路径返回false。第四步收集结果将canDeliver返回false的快递ID收集起来排序后输出。3.3 关键细节与陷阱起点/终点就是禁运城市这是常见边界条件。如果src或dst本身在禁运集合里该快递直接无法送达。需要在BFS开始前就进行判断。城市名格式题目中城市名可能是字符串如“Beijing”也可能是数字字符串如“1”。统一按字符串处理即可但要注意比较时的一致性。道路重复与状态冲突题目数据通常保证一条道路只出现一次。但稳健的代码可以考虑如果同一条道路出现多次且状态不同应以最后一次出现或特定规则为准。不过机试题数据一般很干净。性能考量对于每个快递都做一次BFS假设有K个快递图有V个顶点E条边最坏时间复杂度是O(K*(VE))。在OD机试的约束下通常V, E, K在几百的量级完全可接受。如果规模极大可以考虑更高级的算法但那是后话了。4. 多语言代码解析与实现对比理论讲完了我们来看看具体怎么实现。我会分别用C、Java和Python给出核心代码并分析每种语言实现的特点和注意事项。为了聚焦算法本身这里省略了繁琐的输入字符串解析部分假设数据已经加载到了相应的数据结构中。4.1 C实现详解C版本注重效率和手动控制适合对性能有极致要求的场景。#include iostream #include vector #include string #include unordered_map #include unordered_set #include queue #include algorithm using namespace std; struct Package { string id; string src; string dst; }; bool canDeliver(const string pid, const string src, const string dst, const unordered_mapstring, vectorstring graph, const unordered_mapstring, unordered_setstring restrictions) { // 边界检查起点或终点是否被禁运 auto it restrictions.find(pid); if (it ! restrictions.end()) { const unordered_setstring forbidden it-second; if (forbidden.count(src) || forbidden.count(dst)) { return false; } } // 标准BFS流程 queuestring q; unordered_setstring visited; q.push(src); visited.insert(src); while (!q.empty()) { string cur q.front(); q.pop(); if (cur dst) { return true; } // 遍历邻居 auto graphIt graph.find(cur); if (graphIt graph.end()) continue; // 当前城市没有出边 for (const string next : graphIt-second) { if (visited.count(next)) continue; // 检查禁运 if (it ! restrictions.end() it-second.count(next)) continue; visited.insert(next); q.push(next); } } return false; // BFS结束未找到 } int main() { // 假设数据已读入以下结构中 vectorPackage packages {...}; unordered_mapstring, vectorstring graph {...}; unordered_mapstring, unordered_setstring restrictions {...}; vectorstring undeliverable; for (const auto pkg : packages) { if (!canDeliver(pkg.id, pkg.src, pkg.dst, graph, restrictions)) { undeliverable.push_back(pkg.id); } } // 按题目要求排序输出 sort(undeliverable.begin(), undeliverable.end()); for (const string id : undeliverable) { cout id endl; } return 0; }C实现要点使用STL容器unordered_map和unordered_set提供平均O(1)的查找是性能关键。常量引用传递在canDeliver函数中使用const 传递大的数据结构避免不必要的拷贝。迭代器检查在访问graph和restrictions前使用find检查键是否存在防止operator[]自动插入新键导致逻辑错误或内存浪费。手动管理代码稍显冗长但控制力强性能可预测。4.2 Java实现详解Java版本在清晰度和开发效率上取得平衡是企业级应用的主流选择。import java.util.*; public class CourierDelivery { static class Package { String id; String src; String dst; // 构造方法省略 } public static boolean canDeliver(String pid, String src, String dst, MapString, ListString graph, MapString, SetString restrictions) { SetString forbidden restrictions.getOrDefault(pid, Collections.emptySet()); // 起点终点禁运判断 if (forbidden.contains(src) || forbidden.contains(dst)) { return false; } QueueString queue new LinkedList(); SetString visited new HashSet(); queue.offer(src); visited.add(src); while (!queue.isEmpty()) { String cur queue.poll(); if (cur.equals(dst)) { return true; } ListString neighbors graph.get(cur); if (neighbors null) continue; for (String next : neighbors) { if (visited.contains(next)) continue; if (forbidden.contains(next)) continue; // 禁运城市检查 visited.add(next); queue.offer(next); } } return false; } public static void main(String[] args) { ListPackage packages new ArrayList(); MapString, ListString graph new HashMap(); MapString, SetString restrictions new HashMap(); // ... 数据初始化省略 ListString undeliverable new ArrayList(); for (Package pkg : packages) { if (!canDeliver(pkg.id, pkg.src, pkg.dst, graph, restrictions)) { undeliverable.add(pkg.id); } } Collections.sort(undeliverable); for (String id : undeliverable) { System.out.println(id); } } }Java实现要点getOrDefault方法restrictions.getOrDefault(pid, Collections.emptySet())一行代码优雅地处理了可能不存在的键避免了繁琐的null检查。集合框架HashMap,HashSet,LinkedList(作为Queue) 配合使用API成熟稳定。.equals()比较字符串切记不要用比较字符串内容。代码结构清晰面向对象的思维将Package封装成类逻辑分层明确易于阅读和维护。4.3 Python实现详解Python版本以极致的简洁和开发速度见长是快速原型和笔试的利器。from collections import defaultdict, deque def can_deliver(pid, src, dst, graph, restrictions): 判断快递pid能否从src送达dst forbidden restrictions.get(pid, set()) # 起点终点检查 if src in forbidden or dst in forbidden: return False queue deque([src]) visited {src} while queue: cur queue.popleft() if cur dst: return True for nxt in graph.get(cur, []): # 使用get避免KeyError if nxt in visited: continue if nxt in forbidden: continue visited.add(nxt) queue.append(nxt) return False def main(): packages [...] # 列表元素为(id, src, dst)元组或字典 graph defaultdict(list) # {city: [neighbor1, neighbor2]} restrictions defaultdict(set) # {pid: {forbidden_city1, ...}} # ... 数据加载过程省略 undeliverable [] for pkg in packages: pid, src, dst pkg[id], pkg[src], pkg[dst] if not can_deliver(pid, src, dst, graph, restrictions): undeliverable.append(pid) undeliverable.sort() for pid in undeliverable: print(pid) if __name__ __main__: main()Python实现要点defaultdict神器在构建graph和restrictions时defaultdict(list/set)让添加操作无需检查键是否存在代码异常简洁。deque作为队列collections.deque的popleft()和append()操作是O(1)比用list模拟队列高效得多。dict.get(key, default)安全地访问字典避免KeyError。简洁的语法in运算符用于集合和字典查找非常直观if src in forbidden一目了然。开发效率极高同样的逻辑Python代码行数通常只有C的一半甚至更少在限时机试中优势明显。4.4 语言选型与实战建议追求极致性能与掌控感选C。尤其当图规模极大时C的手动内存管理和STL的高效会带来优势。但需要扎实的语言功底避免内存泄漏和指针错误。面向企业开发与平衡之选选Java。语法严谨生态成熟代码模式规范是大多数大型项目的首选。在机试中表现稳定不易有意外。追求解题速度与简洁性选Python。在算法笔试中Python能让你用更少的时间写出正确的逻辑把精力集中在算法本身而非语言细节上。其强大的内置数据结构字典、集合让图算法的实现变得异常轻松。个人心得在华为OD机试中题目对时间复杂度的要求是统一的不会因为语言不同而改变标准。因此选择你最熟悉、最能稳定发挥的语言至关重要。我见过用C因为一个迭代器错误调试半小时的也见过用Python二十分钟AC的。“熟”生巧远胜于“强”而生疏。5. 常见“踩坑点”与调试技巧即便思路正确实现过程中也难免遇到各种bug。下面是我总结的这道题最容易出错的几个地方以及对应的调试方法。5.1 典型错误场景忽略“无向图”的构建题目说“城市A和城市B之间有道路”通常意味着这是无向边。如果你只在邻接表中添加了graph[A].push_back(B)却忘了graph[B].push_back(A)那么搜索路径就会漏掉一半的方向。这是最常见的错误之一。禁运规则处理不当错误1把禁运规则存成了列表判断时用了O(n)的遍历在数据量大时超时。必须用哈希集合。错误2在BFS中只判断了“下一个城市”是否禁运忘了在BFS开始前判断起点和终点本身是否被禁运。错误3禁运规则可能为空或者某个快递没有禁运规则。访问restrictions[pid]前如果不做检查在C中会导致插入空集在Python中会KeyError可能引发逻辑错误或运行时异常。已访问集合visited使用错误忘记在将节点加入队列时同步加入visited导致同一节点被重复加入队列引发无限循环或性能骤降。错误地在弹出节点时才标记visited这同样会导致节点被重复访问。正确做法在将节点加入队列的那一刻就将其标记为已访问。输入格式解析错误机试的输入通常是字符串需要自己按空格或逗号分割。容易出错的地方包括城市名带空格但题目通常不会数字和字符串的转换以及空行的处理。强烈建议在本地编写一个健壮的parseInput()函数进行测试。输出格式不符题目要求输出无法送达的快递ID可能要求按ID升序排序也可能要求用空格隔开或者每行一个。务必严格按照题目要求的格式输出否则就是“格式错误”功亏一篑。5.2 调试与测试策略构造极端测试用例最小图只有1个城市快递的起点终点相同。无路径图起点和终点在不连通的组件中。全禁运图快递的禁运城市包含了所有可能经过的城市。起点即终点禁运快递的起点就在禁运列表里。大规模数据自己写个脚本生成几百个城市和几千条边的随机数据测试程序是否超时或内存溢出。使用IDE调试器单步跟踪BFS的执行过程查看队列、已访问集合、禁运集合的变化这是定位逻辑错误最直接的方法。打印关键中间状态在无法使用调试器时比如在线笔试环境在代码中关键位置插入打印语句。例如在BFS循环开始时打印当前队列在判断禁运时打印当前城市和禁运集合。对比输出对于复杂用例可以手动推导出几个快递的预期送达结果与程序输出对比。5.3 性能优化小贴士虽然本题数据规模下无需过度优化但养成好习惯有益无害使用局部引用在C/Java的循环中对于容器内取出的对象使用引用或final局部变量避免重复调用getter或产生临时对象。预估容器大小如果已知大概规模在C中可以用reserve为vector预分配空间在Java中可以在创建ArrayList或HashMap时指定初始容量减少扩容开销。Python中使用sys.stdin.readline读取大量输入时这比input()快得多。6. 从解题到举一反三图论问题的通用思考框架搞定一道“快递投放问题”不是终点我们的目标是掌握解决一类图论问题的能力。这类“带约束的连通性/路径搜索”问题变体很多但核心思考框架是相通的。6.1 问题变体与应对策略变体一要求输出具体路径而不仅仅是判断能否送达。解法在BFS/DFS过程中额外维护一个parent字典或数组记录每个节点是从哪个节点访问过来的。当找到终点时从终点反向回溯到起点即可得到路径。注意BFS找到的第一条路径就是最短路径边数最少。变体二道路有“权重”如距离、成本、时间需要找成本最低的送达路径。解法这就变成了带权单源最短路径问题。如果权重非负使用Dijkstra算法如果权重有负值但无负环使用Bellman-Ford算法。禁运规则可以转化为将禁运城市的顶点从图中临时移除或者在松弛Relax步骤前进行判断。变体三有多个快递但运输车容量有限需要规划配送顺序。解法问题升级为**带约束的车辆路径问题VRP**的简化版。这通常需要使用回溯、动态规划甚至启发式算法如遗传算法、模拟退火。在机试中规模会控制得很小可能用状态压缩DP可以解决。变体四道路状态是动态的随时间变化。解法图变成了时间依赖图。需要在传统的BFS/Dijkstra基础上将“时间”作为一个维度。可以使用“状态(city, time)”作为搜索节点在队列或优先队列中传播。6.2 构建你的图论解题工具箱面对新的图论题可以按以下步骤思考建模问题中的实体是什么城市、路口、人实体之间的关系是什么道路、连接、认识把实体抽象成顶点关系抽象成边。定性图是有向还是无向边有没有权重成本、距离权重是正还是负需要找什么连通性、一条路径、最短路径、所有路径、最大流选算法判断连通性、找一条路径 → BFS / DFS。无权图最短路径边数最少 → BFS。带权非负图最短路径 → Dijkstra。带权可能有负权图最短路径 → Bellman-Ford / SPFA。所有顶点对最短路径 → Floyd-Warshall。拓扑排序 → Kahn算法 / DFS。强连通分量 → Kosaraju / Tarjan。加约束像“禁运城市”这类顶点访问限制通常在搜索的扩展步骤查看邻居时作为剪枝条件加入。像“容量限制”这类边上的约束可能需要用到网络流算法。实现与测试选择熟悉的数据结构实现算法并精心设计测试用例验证。这道“快递投放问题”就像一块很好的磨刀石它综合了图的基本遍历、约束处理和业务建模。把它吃透再遇到“社交网络好友推荐”、“网络故障排查”、“游戏地图寻路”等题目时你就能一眼看穿其图论本质快速套用或改编已有的解决方案。编程能力的提升正是在这种一次次将具体问题抽象化又将通用算法具体化的过程中完成的。