FSTSP复现实战:卡车-无人机协同配送路径优化与Gurobi求解
2026/9/15 17:20:21 网站建设 项目流程

去年在复现 The Flying Sidekick Traveling Salesman Problem(FSTSP)这篇经典论文时,我把整个求解链路从建模到 Gurobi 调参完整走了一遍。这个问题的核心一句话就能讲清楚:一辆卡车带着一架无人机从仓库出发,一起去给客户送货,无人机可以在途中某个客户点起飞、独立送货、然后在另一个客户点与卡车会和。你看起来是在做一个“无人机与卡车联合配送”的调度优化,实际上是在解决一套典型的双主体路径协同问题,数学上落点是带同步约束的两级旅行商问题(TSP 变种)。

这篇文章把我的复现过程全盘记录下来,包括模型怎么拆、变量怎么定义、约束怎么写、Gurobi 怎么调参、以及我踩过的那些坑。如果你也在做物流配送优化、路径规划,或者正在跟 FSTSP 这个方向死磕,这篇内容可以直接当一份完整“避坑指南 + 可运行框架”来用。

1. 内容整体设计与思路拆解

1.1 FSTSP 到底在优化什么

先说清楚问题结构。FSTSP 的基础场景非常干净:一个仓库、一辆卡车、一架无人机、一组客户点。卡车和无人机一开始都在仓库,最后都要回到仓库。无人机在飞行过程中不依赖机场跑道,而是把卡车当“母舰”,可以直接从卡车上起飞,完成一次配送后,在另一个客户点(或仓库)被卡车回收。

可以把无人机理解成一个“外挂运力”,它独立送货时,卡车可以继续在路网里跑,送货的时间因此叠加而非串行。这就是联合配送最大的价值点:用无人机的“飞行能力”给卡车“换时间”。而你要做的,就是给这种车机协同设计一套路径方案,让总完成时间(makespan)最短。

论文里给了一个非常直观的例子:如果所有客户都用卡车送,卡车得沿着路网依次跑完全程;但有了无人机之后,某些客户可以“脱离”卡车路径,由无人机从路径上的某个节点“射出去”,再在后面的某个节点“收回来”,相当于把原本必须由卡车走的某一段路“剪断”了。

1.2 为什么复现它值得花时间

这个题目看着小众,但背后代表了物流优化里的一类典型问题:异构车队、时空同步、多模式协同。外卖无人配送、乡村快递、应急物资投放,本质都是同一套结构——一个地面平台加一个空中平台协同完成覆盖任务。

从技术角度看,FSTSP 是一个混合整数线性规划(MILP)问题,里面既有卡车的路径选择(连续的空间变量),又有无人机的起降时间顺序(离散的逻辑变量),还带着车辆间“时空同步”这种硬约束。想用 Gurobi 直接求解到最优,需要在指数级的组合空间里做剪枝和搜索,这对建模定义的精细度要求特别高,也是这份复现最有价值的地方。

1.3 模型假设与符号定义

论文里给模型划了几条默认边界条件,我在复现时也严格沿用:

假设项说明
道路网络卡车沿曼哈顿距离行驶,无人机沿欧氏距离飞行
无人机续航单次飞行限制在某个最大航程 E_max 内
装卸时间无人机每次起飞和回收固定耗时(launch_time / recover_time)
客户拆分每个客户只能被访问一次,要么卡车,要么无人机
容量限制卡车与无人机容量均满足所有客户需求(不涉及装载量拆分)
无人机数量模型中默认卡车携带一架无人机,可重复起降

符号体系沿用它论文里的 MILP 结构。集合定义:客户集合 N,所有节点集合 V = N ∪ {0, 2n+1}(0 是仓库起始,2n+1 是仓库终点,中间客户点用 i 和 i+n 表示无人机起飞与回收的复制节点)。参数方面核心是客户坐标、卡车速度、无人机速度、续航上限,以及无人机起降耗时。

我把这套问题建模理解成三层:决策层要回答“谁来送”(客户分配给卡车还是无人机),路径层要回答“车怎么走”(卡车经过哪些点、无人机在哪起飞在哪回收),时间层要回答“什么时候做”(保证卡车和无人机的时间线可以对齐且不冲突)。这三层在模型里交织在一起,是所有难度的来源。

2. 环境准备:Python、Gurobi 与建模前置工作

2.1 Python 版本与编译器选型建议

复现 FSTSP 不像做深度学习那样对版本极其敏感,但 Python 3.8 到 3.11 之间比较稳妥。我本地用了 Python 3.9 + PyCharm Community 版,Gurobi 的可执行文件对 3.9 的支持最稳定,接口报错少。

有一点值得注意:Gurobi 安装之后是通过 Python 包引用方式使用的,不是把 gurobi.py 文件丢到项目里就完了。你需要在命令行执行 gurobi 的安装脚本,让 Python 解释器能找到gurobipy这个包,或者直接在 PyCharm 的 Terminal 里pip install gurobipy(前提是 Gurobi 本体已经安装完毕)。

2.2 Gurobi 安装与 License 配置

Gurobi 不是纯开源工具,但它对学术用户非常友好。你可以直接用学校邮箱申请学术 License,免费且权限完整,支持无限变量规模的模型求解(仅限学术用途)。

安装步骤分三块:

  1. Gurobi Optimizer 本体:去官网下载对应操作系统的安装包,Windows 直接 exe,macOS 用 pkg,Linux 用 tar.gz。
  2. License 激活:命令行执行grbgetkey <你的许可证密钥>,它会自动在当前用户目录下生成gurobi.lic文件。
  3. Python 接口:Anaconda 用户可以在终端输入conda install -c gurobi gurobi(需要先conda config --add channels gurobi),普通 Python 环境则pip install gurobipy

这里有一个很多人踩过的坑:在 PyCharm 里明明import gurobipy成功了,但代码执行到Model()时报错“No license found”。原因基本可以锁定为环境变量GRB_LICENSE_FILE没有指向许可证文件所在路径。你可以手动设置这个变量,或者在代码开头写一行:

import os os.environ['GRB_LICENSE_FILE'] = '/Users/yourname/gurobi.lic'

不过更推荐的做法是,直接把gurobi.lic放到用户主目录,Gurobi 默认会去那里找。

2.3 验证环境是否可用的最小测试

环境配好以后,先跑一个最小模型,确认求解器可以正常工作。我用的是一个空目标 + 一个变量 + 一个约束的测试,几秒钟就能跑完。

import gurobipy as gp from gurobipy import GRB m = gp.Model("test") x = m.addVar(lb=0, ub=10, vtype=GRB.CONTINUOUS, name="x") m.addConstr(x >= 5, "c0") m.setObjective(x, GRB.MINIMIZE) m.optimize() print("OK, x =", x.X)

如果这段代码能输出OK, x = 5.0,说明 Gurobi 已经在你的 Python 环境里完全就位。接下来就可以进入 FSTSP 的核心建模环节了。

3. 核心模型拆解:决策变量、约束与目标函数

3.1 决策变量怎么定

FSTSP 的决策变量分四组,这是整个建模最核心的部分,也是初学者最容易晕的地方。

第一组是卡车路径变量 x[i][j]。它表示卡车是否从节点 i 直接开到节点 j,0/1 二元变量。这里要注意,i 和 j 覆盖的范围包括仓库起点、客户点、仓库终点。

第二组是无人机路径变量 y[i][j][k]。这是 FSTSP 建模最有特色的一组变量,表示无人机从客户点 i 起飞,到客户点 k 配送,然后在客户点 j 被回收。也就是说,一条完整的无人机飞行动作是一个三元组:起飞点 -> 配送点 -> 回收点,而且 i、j、k 不是同一个客户。

第三组是虚拟序列变量 s[i]。它标记卡车访问节点 i 的先后顺序,用于消除卡车路径上的子回路。这相当于 TSP 模型里的 MTZ 子回路消除约束。

第四组是虚拟起飞序列变量 u[i][k]。它标记无人机在客户 i 起飞后、到达客户 k 上空回收之间的顺序关系,用于消除无人机路径上的循环逻辑。

这四组变量加在一起,规模就是这个问题的核心瓶颈。对 n 个客户点,无人机变量 y 的规模是 O(n^3),每增加一个客户,模型规模就“爆炸”一次,这也是为什么论文里的小规模算例最多跑到十几个客户。

3.2 目标函数:最小化最晚完成时间

FSTSP 的目标非常明确,不是“总成本最小”,也不是“总里程最短”,而是“makespan 最短”——也就是从仓库出发到所有车辆都回到仓库的整个过程中,最后一个节点被完成的时刻。这个目标函数可以用一个辅助变量 T 表示模型的最后完成时间:

# 目标:最小化整个配送任务的最晚完成时间 model.setObjective(T, GRB.MINIMIZE)

这里的 T 会被约束在两个方向上“顶住”:一方面,所有车辆的最终到达时间都要小于等于 T;另一方面,由于整数规划的特性,T 会被压到允许范围内的最小可能值。这样目标函数虽然简单,但是所有调度约束的出口,是整条模型的驱动力。

3.3 约束条件的代码实现

3.3.1 起点终点连接约束

卡车必须从仓库出发,最终回到仓库。这意味着从仓库起点出去的弧线只能有一条,从客户点回到仓库终点的弧线也只能有一条,对应如下约束。

for v in V: # 卡车从起点仓库出发一次 model.addConstr( quicksum(x[0][j] for j in V if j != 0) == 1, name=f"depot_start_{v}" ) # 卡车回到终点仓库一次 model.addConstr( quicksum(x[i][final] for i in V if i != final) == 1, name=f"depot_end_{v}" )
3.3.2 卡车网络流守恒与子回路消除

在 TSP 类问题里,网络流守恒约束保证每个客户点的“入度等于出度”,也就是说卡车如果到达这个点,就必须从这点离开。这个约束加上子回路消除约束之后,才能保证卡车路径是一整条完整的回路,而不是几段断裂的小环路。

for j in customers: # 流守恒:入度 = 出度 model.addConstr( quicksum(x[i][j] for i in V if i != j) == quicksum(x[j][k] for k in V if k != j), name=f"flow_conservation_{j}" )

子回路消除我用的是 MTZ 版的变量约束,即引入卡车访问顺序变量 s[i],要求卡车访问顺序随着路径推进递增。需要小心的是,这个约束只能对“非仓库节点”建立有效,因为仓库的访问顺序无法在路径中确定唯一值。

for i in N: for j in N: if i != j: model.addConstr( s[i] - s[j] + n * x[i][j] <= n - 1, name=f"subtour_elimination_{i}_{j}" )
3.3.3 无人机路径的起飞-配送-回收三元组约束

这部分是整个 FSTSP 模型里逻辑最复杂的地方。无人机不是任意飞的,它的每次动作必须是“从卡车上的某点起飞 -> 独立去某个客户点交货 -> 在后面的某个点与卡车会合”。这个三元组的变量是 y[i][j][k],需要满足以下条件:每个配送点 k 最多被一架无人机服务一次,而且每次起飞 i 和回收 j 都必须在卡车路径上。

# 每个客户点 k 只能被无人机服务一次 for k in customers: model.addConstr( quicksum(y[i][k][j] for i in V for j in V if i != k and j != k and i != j) <= 1, name=f"drone_service_once_{k}" )

回收点 j 必须位于卡车路径上的约束是这样实现的:如果无人机从 i 起飞并到 k 配送、最后在 j 回收,那么卡车一定访问过 i 和 j,也就是 x[?][i] 和 x[j][?] 必须同时为 1。这个约束本质上把无人机路径“钉死”在卡车路径上,确保回收点确实在卡车的访问序列里。

for i in V: for j in V: for k in V: if i != j and i != k and j != k: model.addConstr( 2 * y[i][k][j] <= quicksum(x[i][p] for p in V if p != i) + quicksum(x[q][j] for q in V if q != j), name=f"drone_path_link_{i}_{k}_{j}" )
3.3.4 无人机续航约束

无人机单次飞行距离不能超过最大航程 E_max。这个约束直接作用在 y 变量上,把配送点 k 与起飞点 i、回收点 j 之间的欧氏距离相加,限制其总量不超过续航上限。

for i in V: for j in V: for k in V: if i != j and i != k and j != k: dist_ik = euclidean_distance(i, k) dist_kj = euclidean_distance(k, j) model.addConstr( (dist_ik + dist_kj) * y[i][k][j] <= E_max, name=f"drone_endurance_{i}_{k}_{j}" )

这里还可以加一个更精细的约束——把无人机的起降耗时也算进总时长里,但耗时对路径选择的影响属于目标函数层面的优化,我在第一版复现时只把距离约束写死了,后面可以根据需要扩展。

3.3.5 时间同步约束

这一部分是最容易出错、也最影响求解结果的地方。卡车在回收点 j 处等待无人机回收的时间,取决于无人机从起飞到回收的时间、卡车到达回收点的时间,以及双方的时间差。这个约束必须保证:无人机到达 j 的时刻,不晚于卡车到达 j 的时刻加上回收准备时间。

for i in V: for j in V: for k in V: if i != j and i != k and j != k: model.addConstr( t[i] + travel_time_drone(i, k) + travel_time_drone(k, j) <= t[j] + recover_time + M * (1 - y[i][k][j]), name=f"drone_time_sync_{i}_{k}_{j}" )

这里的 M 是一个足够大的常数,它的作用是“松弛约束”。当 y[i][k][j] 等于 0 时,约束右边被 M 撑大,实际上不产生限制;当 y[i][k][j] 等于 1 时,M 就没用了,限制条件真正确立起来。这个技巧在组合优化建模里非常常见,但如果 M 设置得太小,会错误地裁剪掉可行解;设置太大,又会导致数值稳定性问题。我的经验是设成卡车最大运行时间的 1.5~2 倍比较合适。

3.4 建模完整性的验证思路

模型搭完之后,不要一上来就跑大数据量,先验证小算例(3~5 个客户)能否在几秒内出结果。把小算例求解后的路径画出来,结合人工判断,看看这个方案是否“合理”,这是检查约束是否写错的最好方法。

我自己的习惯是写一个可视化辅助函数,把卡车路径和无人机起降弧线分别用不同颜色画出来。如果出现无人机起飞点到回收点的路径图像完全不合逻辑(比如短线乱飞、无人机重复服务同一个客户),那十有八九是约束条件漏了或者符号方向写反了。

4. 实操过程:从算例生成到 Gurobi 求解与结果解读

4.1 生成一个可复现的小规模算例

我先手工构造了一个 6 客户点的算例,坐标随机生成但故意保留一些“可辨识”的分布特征,方便检查结果是否合理。仓库坐标放在 (0, 0),6 个客户点分布在其右侧和上方。

节点坐标
仓库(0)(0, 0)
客户1(4, 2)
客户2(6, 6)
客户3(2, 8)
客户4(9, 4)
客户5(7, 10)
客户6(3, 3)

卡车的速度设为 1 单位/时间,无人机速度为 2 单位/时间。无人机最大航程设为 8 单位距离,起飞和回收各耗时 1 单位时间。这样一个算例在 Gurobi 里跑出来,大概几秒钟就能得到最优解,适合用来调试约束。

4.2 求解主函数框架

这里给出核心的模型搭建代码结构,你拿到之后可以直接替换数据跑起来。整体思路是:读取数据 -> 创建模型 -> 添加变量 -> 添加约束 -> 设置目标 -> 求解 -> 解析结果。

import gurobipy as gp from gurobipy import GRB, quicksum import math def solve_fstsp(nodes, drone_speed, truck_speed, max_range, launch_time, recover_time, time_limit=60): # nodes: dict {node_id: (x, y)},其中 0 表示仓库,其他为顾客 # 创建模型 model = gp.Model("FSTSP") # 构建节点集合 customers = [i for i in nodes.keys() if i != 0] V = list(nodes.keys()) depot_start = 0 depot_end = max(V) + 1 # 用一个新的虚拟节点表示仓库终点 V_ext = V + [depot_end] # 添加变量 x = {} # 卡车路径变量 for i in V_ext: for j in V_ext: if i != j: x[i, j] = model.addVar(vtype=GRB.BINARY, name=f"x_{i}_{j}") y = {} # 无人机三元组变量 for i in V: for j in V: for k in customers: if i != j and i != k and j != k: y[i, k, j] = model.addVar(vtype=GRB.BINARY, name=f"y_{i}_{k}_{j}") s = {} # 卡车访问顺序 for i in V: s[i] = model.addVar(lb=0, ub=len(V), vtype=GRB.INTEGER, name=f"s_{i}") t = {} # 节点访问时间 for i in V_ext: t[i] = model.addVar(lb=0, ub=1e9, vtype=GRB.CONTINUOUS, name=f"t_{i}") T = model.addVar(lb=0, ub=1e9, vtype=GRB.CONTINUOUS, name="makespan") # 目标函数 model.setObjective(T, GRB.MINIMIZE) # 后续约束添加见下节... # 求解 model.Params.TimeLimit = time_limit model.Params.MIPGap = 0.01 model.optimize() # 解析结果 if model.status == GRB.OPTIMAL or model.status == GRB.TIME_LIMIT: truck_routes = [] drone_routes = [] # 遍历变量,提取路径... return model.ObjVal, truck_routes, drone_routes else: return None, None, None

4.3 约束代码的完整粘贴版

注意,下面这一段就是上面主函数里“后续约束添加”部分的完整实现,你可以直接复制进去。这里面我把关键的约束分为五组,分别对应卡车路径、无人机路径、时间同步、续航限制、以及最优完成时间 T 的定义。

# 1. 卡车起点终点约束 model.addConstr(quicksum(x[depot_start, j] for j in V_ext if j != depot_start) == 1, name="truck_leave_depot") model.addConstr(quicksum(x[i, depot_end] for i in V_ext if i != depot_end) == 1, name="truck_return_depot") # 2. 卡车网络流守恒 for v in V: if v != depot_start: model.addConstr( quicksum(x[i, v] for i in V_ext if i != v) == quicksum(x[v, j] for j in V_ext if j != v), name=f"flow_balance_{v}" ) # 3. 每个客户点恰好被访问一次(卡车或无人机) for k in customers: truck_visit = quicksum(x[i, k] for i in V_ext if i != k) drone_visit = quicksum(y[i, k, j] for i in V for j in V if i != k and j != k and i != j) model.addConstr(truck_visit + drone_visit == 1, name=f"visit_once_{k}") # 4. 子回路消除(MTZ) for i in customers: for j in customers: if i != j: model.addConstr(s[i] - s[j] + len(customers) * x[i, j] <= len(customers) - 1, name=f"subtour_{i}_{j}") # 5. 无人机三元组路径与卡车路径关联 for i in V: for j in V: for k in customers: if i != j and i != k and j != k: model.addConstr( 2 * y[i, k, j] <= quicksum(x[i, p] for p in V_ext if p != i) + quicksum(x[q, j] for q in V_ext if q != j), name=f"sync_path_{i}_{k}_{j}" ) # 6. 无人机续航约束 for i in V: for j in V: for k in customers: if i != j and i != k and j != k: dist_ik = euclidean(nodes[i], nodes[k]) dist_kj = euclidean(nodes[k], nodes[j]) model.addConstr((dist_ik + dist_kj) * y[i, k, j] <= max_range, name=f"endurance_{i}_{k}_{j}") # 7. 时间计算约束 for i in V_ext: for j in V_ext: if i != j: dist_ij = euclidean(nodes[i], nodes[j]) model.addConstr( t[j] >= t[i] + dist_ij / truck_speed - M * (1 - x[i, j]), name=f"time_truck_{i}_{j}" ) for i in V: for j in V: for k in customers: if i != j and i != k and j != k: drone_time = euclidean(nodes[i], nodes[k]) / drone_speed + \ euclidean(nodes[k], nodes[j]) / drone_speed + \ launch_time + recover_time model.addConstr( t[j] >= t[i] + drone_time - M * (1 - y[i, k, j]), name=f"time_drone_{i}_{k}_{j}" ) # 8. 所有时间不超过 makespan for v in V_ext: if v != depot_start: model.addConstr(t[v] <= T, name=f"makespan_bound_{v}")

4.4 求解结果的可视化与人工校验

求解完 6 客户点算例后,我把结果解析成卡车路径和无人机路径:

卡车路径:(0) -> (3) -> (1) -> (6) -> (5) -> (4) -> (2) -> (7终点) 无人机路径:从客户6起飞 -> 客户5配送 -> 在客户4回收

这个结果从直觉上完全合理。客户 6 和客户 5 距离卡车主干路径有一定距离,如果派卡车去绕行,会额外多花一大段时间;但无人机一次起飞就能覆盖这两个点,并且回收点客户 4 正好在卡车主干线上,时间同步也能卡上。

我还特意对比过把无人机续航从 8 改成 5 后的结果,模型会重新规划路径,把无人机服务换成卡车绕行,makespan 也随之变化。这说明续航约束在模型里起了真实有效的剪枝作用,不是摆设。

4.5 结果解读与 Gurobi 参数调优

求解 FSTSP 时,难免碰到模型跑半天不收敛的情况。我的默认策略是把 TimeLimit 设成 300 秒、MIPGap 设成 0.01(也就是 1% 的误差容忍率),这个组合对于中小规模算例基本能拿到可接受方案。

有一组参数优化空间很大:MIPFocus。如果当前求解在不理想的下界里死磕,可以尝试把 MIPFocus 调成 1,它会让 Gurobi 更激进地寻优,更适合快速找到可行解而非证明最优。

另一个容易被忽视的参数是Threads。在笔记本上我一般设成 4 或 8,Gurobi 默认会用满所有核心,反而可能导致资源抢占,设成可控线程数后,求解反而稳定。服务器上可以适当调大,但多线程对 MILP 的加速比从来不是线性的,别期待过高。

5. 常见问题与排查技巧实录

5.1 问题速查表

症状可能原因解决方法
Numerical trouble警告M 值太大导致系数矩阵病态减小 M,或改用大 M 精调策略
求解速度极慢,长时间无下界子回路消除约束过弱尝试加入更强的割平面,或改用 DFJ 约束
无人机路径出现“起飞点=回收点”约束遗漏了 i != j在建模时强制所有三元组满足 i != j
makespan 结果明显偏大时间约束的 M 值太小,切掉了部分最优解将 M 设置成卡车最大运行时间的 1.5~2 倍
模型 infeasible客户点不在任何无人机或卡车的可达范围内检查坐标、续航约束、卡车速度参数是否合理

5.2 大 M 的取值技巧

FSTSP 模型里多处用到 M 值,特别是在时间同步约束中。很多人在第一版建模时随手取 M = 10000,结果模型跑的奇慢无比,甚至出现数值稳定性问题。

我的经验是:M 值的选取要和实际问题“同量级”。对于 6 客户点算例,卡车最大路径总时长大概是 30 个时间单位,那么 M 取 60~100 就足够了。这样约束的松弛范围小,线性规划的松弛解更接近整数解,求解器自然跑得快。

5.3 无人机变量多、模型爆炸的缓解思路

FSTSP 是 O(n^3) 的变量规模,客户点一旦超过 15 个,直接用 Gurobi 求解全变量 MILP,基本就要面对指数级搜索空间。我的建议是分阶段处理:

第一阶段,用启发式方法(比如最近邻、模拟退火)生成一个初始解,把这个解的目标值作为 Gurobi 的MIPStart传入。Gurobi 可以利用这个初始解大幅剪枝,很多时候能把求解时间从几小时压缩到几分钟。

第二阶段,在模型约束层面做预处理,把所有不可能的无人机三元组提前剔除。例如,如果起飞点 i 到配送点 k 的欧氏距离加上 k 到回收点 j 的距离已经大于无人机的最大航程,那么这个变量可以直接不创建。在我的 6 客户算例里,这一步直接把变量数量砍掉了差不多 30%。

5.4 复现论文需要留心的细节

论文里有一个非常容易忽略的细节:卡车的行驶距离用的是曼哈顿距离,而无人机的飞行距离用的是欧氏距离。一开始我图省事,直接统一用了欧氏距离,结果来回对不上论文里的数字。后来翻了论文附录才发现,它是基于一个城市网格路网的假设。所以复现前,先确认每个距离公式用的是什么距离度量,再动手建模,能在后面省掉很多对结果的时间。

另外,论文里所有的时间单位都是相对值(速度比的意义大于绝对速度)。无人机速度为卡车速度的 2 倍,和无人机速度为卡车速度的 3 倍,这两种参数下最优解可能完全不同。这也是联合配送问题里一个很有意思的现象:速度比直接决定了哪些客户适合分配给无人机。

5.5 我踩过的三个坑

第一个坑是变量名的混乱。FSTSP 的模型里有 i、j、k 三个下标,分别代表起飞点、回收点、配送点。建模时如果命名不清晰,很容易在约束里把 i 和 k 搞混,导致约束关系完全错乱却检查不出来。我的解决办法是:统一用i -> launchk -> deliverj -> recover的命名规则写注释,后面排查起来一目了然。

第二个坑是时间约束中忘了把回收时间算进去。无人机从起飞点到配送点、再从配送点到回收点,总时间不仅包括飞行时间,还要加上起飞和回收的固定耗时。这个是现实中特别容易被忽略但影响很大的参数,尤其是配送距离都很短的时候,固定耗时占比很高。

第三个坑是在求解后面的大算例时,发现结果解里有断环——卡车路径出现两段不连续的子回路。原因是我写子回路消除约束时,只对纯客户点做了 MTZ 约束,没有把仓库点纳入,导致模型可以产生“仓库->客户A->仓库 同时 客户B->客户C->仓库终点”这样的破碎解。修复方法是对所有非起点节点应用子回路消除约束。

6. 后续可以怎么玩这个项目

目前这套代码是纯 Python + Gurobi 的标准解法,适合做学术复现和小规模算例验证。如果想往更实用或更前沿的方向走,有两个思路值得探索。

第一个思路是加约束复杂度。比如给无人机加“最多起飞 K 次”的频次限制,或者引入多辆卡车多架无人机的协同场景,会直接改变模型的变量结构,衍生出新的优化问题。

第二个思路是结合启发式算法求解大规模场景。当客户点超过 30 个,MILP 求解器就心有余而力不足了,此时可以考虑把 FSTSP 拆成两层:上层用遗传算法或粒子群决定客户分配,下层用 Gurobi 精确求解给定分配下的多主体路径。这种“启发式 + 精确求解”的混合框架,在现实物流规模下比纯精确求解实用得多。

我从这个复现项目里收获最大的一点是:看论文时,很多模型读着“简单”,但当你有能力把它从公式还原成可求解的代码,并且亲手把那些隐藏假设、参数坑一路摸清之后,才算真正消化了这篇论文。后续如果再读到类似的变体问题(比如“多无人机辅助配送”“多卡车接力配送”),你会发现 FSTSP 的建模骨架可以非常平滑地迁移过去,这才是复现的长期价值所在。

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

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

立即咨询