订单配送路径规划(2026.11.08)
2026年最新华为OD机考题库(按考点分类): 2026年最新华为OD机考题库
题目描述
在智能物流配送系统中,配送员需要按照最优路径完成多个订单的配送任务。
给定多个订单信息,每个订单包含:位置坐标 (x, y);时间窗口 [startTime, endTime],必须在指定时间范围内完成配送;订单重量 weight。
请根据如下约束条件,规划出一条能够完成所有订单配送的最优路径:
- 起点与终点:配送员从起点 (0, 0) 出发,需访问所有订单位置,最后返回起点 (0, 0);
- 移动规则:每次只能移动到相邻位置(上下左右四个方向),移动一个单位距离消耗 1 个单位时间;
- 载重限制:配送员有最大载重限制 capacity,出发时车辆装载所有订单货物,要求所有订单的总重量之和不超过 capacity;若总重量超过 capacity,则无法执行任务;
- 时间窗口约束:每个订单必须在时间窗口 [startTime, endTime] 内完成配送,不允许提前到达后等待。即:配送员到达订单位置的时刻(即配送完成时刻)必须满足 startTime <= arrival_time <= endTime,若 arrival_time < startTime 或 arrival_time > endTime,均视为不满足约束。
- 移动时间:途中不能停留,也不能绕路拖延时间,从一个位置走到下一个位置所用的时间就是两点的曼哈顿距离 |x1-x2|+|y1-y2|;订单的配送顺序可以任意安排,途中经过其他订单的位置不算配送;最后返回起点那一段不受时间窗口限制。
输入描述
第一个参数 orders 是二维数组,每个元素为 [x, y, startTime, endTime, weight];第二个参数 capacity 是整数,表示最大载重限制。
数据范围
- 1 <= orders.length <= 10
- 0 <= x, y <= 50
- 0 <= startTime <= endTime <= 1000
- 1 <= weight <= 100
- 1 <= capacity <= 10^4
输出描述
返回一个整数,表示完成所有配送并返回起点所需的最短总时间;无法完成时返回 -1。
样例
用例输入 1
[[1, 2, 0, 5, 2], [3, 1, 2, 8, 3], [2, 3, 4, 10, 1]],5
用例输出 1
-1
用例解释 1
总重量 2+3+1=6 > capacity=5,返回 -1。
用例输入 2
[[1, 1, 0, 10, 2], [2, 2, 3, 15, 2]],4
用例输出 2
8
用例解释 2
(0,0)->(1,1) 距离 2、时间 2、载重 2,落在 [0,10];(1,1)->(2,2) 距离 2、时间 4、载重 4,落在 [3,15];(2,2)->(0,0) 距离 4、时间 8。
用例输入 3
[[0, 0, 0, 5, 1]],10
用例输出 3
0
用例解释 3
订单在起点,不需要移动,时间 0 在窗口 [0,5] 内。
来源
本题来源于网络、非官方材料、仅供学习交流,侵删
🧠 解题思路
第一步:把题目转化成“排列问题”
先把规则翻译一下:
- 配送员从 (0,0) 出发,订单可以按任意顺序送,送完再回到 (0,0);
- 从一个位置走到下一个位置,花的时间就是两点的曼哈顿距离 |x1-x2|+|y1-y2|,途中不能停、不能绕路;
- 到达每个订单的时刻必须落在它的时间窗 [startTime, endTime] 内;
- 载重只看一件事:所有订单的总重量不能超过 capacity,超过就直接返回 -1。
所以一旦确定了配送顺序(订单的一个排列),每一单的到达时刻就完全确定了:把一路上的距离依次加起来即可。题目要的,就是在“每一单都落在时间窗内”的排列里,让“最后一单的到达时刻 + 回起点的距离”最小。
第二步:暴力枚举排列为什么不够好
订单最多 10 个,排列有 10! = 3628800 种,每种排列还要走 10 步检查时间窗。
暴力慢在哪里?很多排列的“前半段”是一样的。比如先送 {A,B,C} 三单、最后停在 C,后面怎么走只和“送了哪些单、停在哪、现在几点”有关,和前面三单的先后顺序无关。把这些相同的局面合并起来只算一次,就是状态压缩 DP。
第三步:状态压缩是什么——用二进制数表示“送了哪些单”
n 个订单,每一单“送了/没送”两种情况,可以用一个 n 位二进制数 mask 表示:第 i 位是 1 表示订单 i 已经送了。例如 n = 3 时:
| mask(十进制) | 二进制 | 含义 |
|---|---|---|
| 1 | 001 | 只送了订单 0 |
| 5 | 101 | 送了订单 0 和订单 2 |
| 7 | 111 | 三单全送完 |
常用的两个操作:
- 判断订单 j 送没送:看 (mask >> j) & 1 是不是 1;
- 送了订单 j 之后的新集合:mask | (1 << j)。
n ≤ 10 时 mask 最多 2^10 = 1024 种,非常小。
第四步:只记“最早到达时刻”为什么不行
很多路径题会定义“已送集合为 mask、停在 i 时的最早时刻”,因为一般来说越早越好。但本题不能等待:到早了不能停下来等,晚到反而可能正好赶上下一单的时间窗。看一个例子:
订单 A(0,1) 窗口 [0,14],B(2,2) 窗口 [4,14],C(1,3) 窗口 [4,4],D(0,0) 窗口 [14,15]。
| 路线 | 到 A | 到 C | 到 B | 到 D |
|---|---|---|---|---|
| A→C→B→D | 1 | 4 | 6 | 10,早于 14,不合法 |
| C→A→B→D | 7 | 4 | 10 | 14,合法 |
两条路线送完 {A,B,C} 后都停在 B,一个是时刻 6,一个是时刻 10。如果只记最早的 6,就会认为送不了 D,错误地返回 -1;正确答案是 14 + 0 = 14。
所以状态里必须带上“现在的时刻”。好在时间窗右端点不超过 1000,到达时刻只可能是 0~1000,可以全部记下来。
第五步:定义状态和转移
can[mask][i][t] 表示:已送订单集合为 mask,最后送的是订单 i,并且恰好在时刻 t 到达订单 i,这种情况是否可能。(Python 代码里用集合 reach[mask][i] 存所有为真的 t,写法更短,意思一样。)
- 初始化:从起点直接去订单 i,到达时刻 t = |x_i| + |y_i|,如果 t 在订单 i 的时间窗内,就把 can[1<<i][i][t] 设为真;
- 转移:对每个为真的 can[mask][i][t],枚举还没送的订单 j,到达时刻 arrive = t + dist(i, j)。如果 arrive 在订单 j 的时间窗内,就把 can[mask | (1<<j)][j][arrive] 设为真;
- 枚举顺序:mask | (1<<j) 一定比 mask 大,所以 mask 从小到大枚举,用到一个状态时它已经算完了。
第六步:统计答案
全部送完时 mask = 2^n - 1。对每个最后订单 i,取它最早的可能到达时刻 t,加上回起点的距离 |x_i| + |y_i|(回程没有时间窗)。所有候选里取最小值;一个都没有就返回 -1。
用样例 1 走一遍
orders = [[1,2,0,5,2],[3,1,2,8,3],[2,3,4,10,1]],capacity = 5。
| 步骤 | 计算 | 结果 |
|---|---|---|
| 1 | 总重量 2+3+1 | 6 |
| 2 | 6 > capacity = 5 | 出发时就装不下 |
| 3 | 不必再做 DP | 返回 -1 |
再用样例 2 看看 DP 怎么转移
orders = [[1,1,0,10,2],[2,2,3,15,2]],capacity = 4。总重量 4 不超载;两单之间距离 2。
| 步骤 | 操作 | 新状态 |
|---|---|---|
| 初始化 | 起点→订单 0,时刻 2,在 [0,10] 内 | mask=01,停在 0,时刻 {2} |
| 初始化 | 起点→订单 1,时刻 4,在 [3,15] 内 | mask=10,停在 1,时刻 {4} |
| 转移 | 订单 0(时刻 2)→订单 1,时刻 2+2=4,在 [3,15] 内 | mask=11,停在 1,时刻 {4} |
| 转移 | 订单 1(时刻 4)→订单 0,时刻 4+2=6,在 [0,10] 内 | mask=11,停在 0,时刻 {6} |
| 答案 | 停在 1:4+ | 2 |
最终返回 8,与样例一致。
⚠️ 易错点
- 不能只记“最早到达时刻”:本题不能等待,晚到可能正好赶上下一单的时间窗,必须把每个可能的时刻都保留下来。
- 载重只检查一次:所有订单总重量超过 capacity 就直接返回 -1,与配送顺序无关。
- 到达时刻要同时满足 startTime <= t <= endTime,早到和晚到都不合法。
- 回到起点的那一段没有时间窗,不要再做检查。
- 订单可以就在起点 (0,0),也可以有多单在同一个位置,此时两单之间距离为 0,到达时刻相同。
- mask 要从小到大枚举,保证用到一个状态之前它已经被算完。
⏱️ 复杂度
时间复杂度:O(2^n · n² · T),T = 1001 是可能的时刻个数。n = 10 时最多约 1 亿次简单判断,而且只有落在时间窗内的时刻才会继续转移,实际远少于这个数。
空间复杂度:O(2^n · n · T),n = 10 时约 1024 × 10 × 1001 ≈ 1000 万个布尔值(约 10 MB)。
💻 参考代码(Java)
class Solution {
public int minDeliveryTime(int[][] orders, int capacity) {
int n = orders.length;
// 出发时要装下全部货物:总重量超过载重,直接返回 -1
int total = 0;
for (int[] o : orders) total += o[4];
if (total > capacity) return -1;
// dist[i][j]:订单 i 到订单 j 的曼哈顿距离,也就是走过去要花的时间
int[][] dist = new int[n][n];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
dist[i][j] = Math.abs(orders[i][0] - orders[j][0]) + Math.abs(orders[i][1] - orders[j][1]);
int full = (1 << n) - 1;
// 时间窗右端点不超过 1000,到达时刻只可能是 0..1000
int maxT = 1000;
// can[mask][i][t]:已送订单集合为 mask、最后送订单 i、在时刻 t 到达,这种情况是否可能
// 不能等待,晚到有时反而能赶上后面订单的时间窗,所以要按时刻分别记录,不能只记最早时刻
boolean[][][] can = new boolean[1 << n][n][maxT + 1];
for (int i = 0; i < n; i++) {
int t = Math.abs(orders[i][0]) + Math.abs(orders[i][1]);
if (orders[i][2] <= t && t <= orders[i][3]) can[1 << i][i][t] = true;
}
// mask 从小到大枚举:mask | (1 << j) 一定比 mask 大,用到它之前它已经算完了
for (int mask = 1; mask <= full; mask++) {
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) == 0) continue;
// 在订单 i 的到达时刻一定落在它自己的时间窗内
for (int t = orders[i][2]; t <= orders[i][3]; t++) {
if (!can[mask][i][t]) continue;
for (int j = 0; j < n; j++) {
if ((mask >> j & 1) == 1) continue; // 订单 j 已经送过
int arrive = t + dist[i][j];
// 到达时刻必须落在订单 j 的时间窗内
if (orders[j][2] <= arrive && arrive <= orders[j][3]) {
can[mask | (1 << j)][j][arrive] = true;
}
}
}
}
}
// 全部送完后回到起点,返回段没有时间窗,取最早的到达时刻加上回程距离
int ans = -1;
for (int i = 0; i < n; i++) {
for (int t = orders[i][2]; t <= orders[i][3]; t++) {
if (can[full][i][t]) {
int cand = t + Math.abs(orders[i][0]) + Math.abs(orders[i][1]);
if (ans == -1 || cand < ans) ans = cand;
break;
}
}
}
return ans;
}
}
💻 参考代码(C++)
class Solution {
public:
int minDeliveryTime(vector<vector<int>>& orders, int capacity) {
int n = orders.size();
// 出发时要装下全部货物:总重量超过载重,直接返回 -1
int total = 0;
for (auto& o : orders) total += o[4];
if (total > capacity) return -1;
// dist[i][j]:订单 i 到订单 j 的曼哈顿距离,也就是走过去要花的时间
vector<vector<int>> dist(n, vector<int>(n));
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
dist[i][j] = abs(orders[i][0] - orders[j][0]) + abs(orders[i][1] - orders[j][1]);
int full = (1 << n) - 1;
// 时间窗右端点不超过 1000,到达时刻只可能是 0..1000
const int T = 1001;
// can[(mask * n + i) * T + t]:已送集合为 mask、最后送订单 i、在时刻 t 到达,是否可能
// 不能等待,晚到有时反而能赶上后面订单的时间窗,所以要按时刻分别记录,不能只记最早时刻
vector<char> can((size_t)(1 << n) * n * T, 0);
auto id = [&](int mask, int i, int t) { return ((size_t)mask * n + i) * T + t; };
for (int i = 0; i < n; i++) {
int t = abs(orders[i][0]) + abs(orders[i][1]);
if (orders[i][2] <= t && t <= orders[i][3]) can[id(1 << i, i, t)] = 1;
}
// mask 从小到大枚举:mask | (1 << j) 一定比 mask 大,用到它之前它已经算完了
for (int mask = 1; mask <= full; mask++) {
for (int i = 0; i < n; i++) {
if (!(mask >> i & 1)) continue;
// 在订单 i 的到达时刻一定落在它自己的时间窗内
for (int t = orders[i][2]; t <= orders[i][3]; t++) {
if (!can[id(mask, i, t)]) continue;
for (int j = 0; j < n; j++) {
if (mask >> j & 1) continue; // 订单 j 已经送过
int arrive = t + dist[i][j];
// 到达时刻必须落在订单 j 的时间窗内
if (orders[j][2] <= arrive && arrive <= orders[j][3]) {
can[id(mask | (1 << j), j, arrive)] = 1;
}
}
}
}
}
// 全部送完后回到起点,返回段没有时间窗,取最早的到达时刻加上回程距离
int ans = -1;
for (int i = 0; i < n; i++) {
for (int t = orders[i][2]; t <= orders[i][3]; t++) {
if (can[id(full, i, t)]) {
int cand = t + abs(orders[i][0]) + abs(orders[i][1]);
if (ans == -1 || cand < ans) ans = cand;
break;
}
}
}
return ans;
}
};
💻 参考代码(Python)
class Solution:
def minDeliveryTime(self, orders, capacity):
n = len(orders)
# 出发时要装下全部货物:总重量超过载重,直接返回 -1
total = 0
for o in orders:
total += o[4]
if total > capacity:
return -1
# dist[i][j]:订单 i 到订单 j 的曼哈顿距离,也就是走过去要花的时间
dist = [[abs(a[0] - b[0]) + abs(a[1] - b[1]) for b in orders] for a in orders]
full = (1 << n) - 1
# reach[mask][i]:已送订单集合为 mask、最后送的是订单 i 时,所有可能的到达时刻
# 不能等待,晚到有时反而能赶上后面订单的时间窗,所以每个可能的时刻都要留着,不能只留最早的
reach = [[set() for _ in range(n)] for _ in range(1 << n)]
for i in range(n):
t = abs(orders[i][0]) + abs(orders[i][1])
if orders[i][2] <= t <= orders[i][3]:
reach[1 << i][i].add(t)
# mask 从小到大枚举:mask | (1 << j) 一定比 mask 大,用到它之前它已经算完了
for mask in range(1, full + 1):
for i in range(n):
times = reach[mask][i]
if not times:
continue
for j in range(n):
if mask >> j & 1:
continue # 订单 j 已经送过
start, end = orders[j][2], orders[j][3]
target = reach[mask | (1 << j)][j]
for t in times:
arrive = t + dist[i][j]
# 到达时刻必须落在订单 j 的时间窗内
if start <= arrive <= end:
target.add(arrive)
# 全部送完后回到起点,返回段没有时间窗,取最早的到达时刻加上回程距离
ans = -1
for i in range(n):
if reach[full][i]:
cand = min(reach[full][i]) + abs(orders[i][0]) + abs(orders[i][1])
if ans == -1 or cand < ans:
ans = cand
return ans
💻 参考代码(JavaScript)
class Solution {
minDeliveryTime(orders, capacity) {
const n = orders.length;
// 出发时要装下全部货物:总重量超过载重,直接返回 -1
let total = 0;
for (const o of orders) total += o[4];
if (total > capacity) return -1;
// dist[i][j]:订单 i 到订单 j 的曼哈顿距离,也就是走过去要花的时间
const dist = orders.map(a => orders.map(b => Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1])));
const full = (1 << n) - 1;
// 时间窗右端点不超过 1000,到达时刻只可能是 0..1000
const T = 1001;
// can[(mask * n + i) * T + t]:已送集合为 mask、最后送订单 i、在时刻 t 到达,是否可能
// 不能等待,晚到有时反而能赶上后面订单的时间窗,所以要按时刻分别记录,不能只记最早时刻
const can = new Uint8Array((1 << n) * n * T);
const id = (mask, i, t) => (mask * n + i) * T + t;
for (let i = 0; i < n; i++) {
const t = Math.abs(orders[i][0]) + Math.abs(orders[i][1]);
if (orders[i][2] <= t && t <= orders[i][3]) can[id(1 << i, i, t)] = 1;
}
// mask 从小到大枚举:mask | (1 << j) 一定比 mask 大,用到它之前它已经算完了
for (let mask = 1; mask <= full; mask++) {
for (let i = 0; i < n; i++) {
if (!(mask >> i & 1)) continue;
// 在订单 i 的到达时刻一定落在它自己的时间窗内
for (let t = orders[i][2]; t <= orders[i][3]; t++) {
if (!can[id(mask, i, t)]) continue;
for (let j = 0; j < n; j++) {
if (mask >> j & 1) continue; // 订单 j 已经送过
const arrive = t + dist[i][j];
// 到达时刻必须落在订单 j 的时间窗内
if (orders[j][2] <= arrive && arrive <= orders[j][3]) {
can[id(mask | (1 << j), j, arrive)] = 1;
}
}
}
}
}
// 全部送完后回到起点,返回段没有时间窗,取最早的到达时刻加上回程距离
let ans = -1;
for (let i = 0; i < n; i++) {
for (let t = orders[i][2]; t <= orders[i][3]; t++) {
if (can[id(full, i, t)]) {
const cand = t + Math.abs(orders[i][0]) + Math.abs(orders[i][1]);
if (ans === -1 || cand < ans) ans = cand;
break;
}
}
}
return ans;
}
}
module.exports = Solution;
384



被折叠的 条评论
为什么被折叠?



