案例 039 三个时间窗打架:放弃一单、全部迟到与改序的软窗取舍
案例 039 三个时间窗打架:放弃一单、全部迟到与改序的软窗取舍
摘要:12:00 手上有 5 单落在 3 个点位:O1、O2 承诺 12:05,O3–O5 是同一写字楼的公司拼单、12:10 三份同到。三个窗首尾相接、空间分居三向,可行域为空。本文建带双侧软窗的开放式路径 MILP:弧变量 \(y_{ij}\) 定序、\(u_i\) 定是否配送、\(t_i\) 为到达时刻、\(\delta_i\) 为越界量,目标为 0.6 元/km 的行驶成本 + 8 元/(单·min) 的违约罚(同址三单权重 3)+ 38 元/单的甩单赔付。四方案对比:改序 21.23 元最优(公司晚 0.60 min、O2 晚 0.64 min),甩 O2 39.41 元、甩 O1 39.56 元、原序全晚 225.7 元。甩单与改序的翻转门槛 15.5 元/(单·min);同址三单合并成超级点后 0-1 变量由 30 降到 16,目标值不变。
关键词:外卖配送;软时间窗;区间调度;违约量;Gurobi
1 问题提出
1.1 问题是什么
12:00 整,外卖小哥在体育西路路口,手上 5 单,落在 3 个点上:O1、O2 两单承诺 12:05 送达,O3、O4、O5 是同一栋写字楼 15 层的公司拼单,12:10 三份一起到。三张时间窗在时间轴上首尾相接、在空间上分居三个方向,这就是"三个时间窗打架"。
数据见表 1。按 20 km/h 折算 3 min/km,单点直达时间分别是 O1 1.96 min、O2 3.32 min、公司 7.01 min——每一单单看都来得及(窗口分别是 5、5、11 min)。可一旦串起来,"O1→O2→公司"最早 11.60 min 才到公司,比 12:10 晚 0.60 min;"公司→O1→O2"则让两单 12:05 各晚 10.60 与 14.29 min。
表 1 五单数据(时刻以 12:00 为零点)
| 订单 | 送达点 | x / km | y / km | 距出发点 / km | 窗口 / min | 停靠 / min | 配送费 / 元 |
|---|---|---|---|---|---|---|---|
| O1 | O1 天河城 A 座 | 0.55 | 0.35 | 0.65 | [0, 5] | 1.5 | 7.5 |
| O2 | O2 体育东小区 | 0.35 | 1.05 | 1.11 | [0, 5] | 1.5 | 7.0 |
| O3 | O3 富力中心 15F | 1.75 | 1.55 | 2.34 | [8, 11] | 3.5 | 9.5 |
| O4 | O4 富力中心 15F | 1.75 | 1.55 | 2.34 | [8, 11] | 0.0 | 9.0 |
| O5 | O5 富力中心 15F | 1.75 | 1.55 | 2.34 | [8, 11] | 0.0 | 8.5 |
核心问题:可行域是空的——不存在一条路线能让 5 单全部落在窗内。既然必然违约,就该问:违谁的约、违多少、还是干脆甩一单?

图 1 三个送达点的空间结构与两条典型路线(虚线=公司置顶的原序,实线=改序)
1.2 为什么要优化
- 同址三单把违约成本放大三倍:公司那组晚 1 min,是三份餐同时凉、三个客户同时点差评,违约量按 3 计。合并成超级点只是少建变量,权重不能合并掉。
- "全部迟到"的代价高一个量级:原序(公司置顶)总代价 225.7 元,改序只要 21.23 元,差 204 元。
- 甩单不是免费的:一单赔付 38 元,而改序的全部违约罚只有 19.51 元。只有当违约罚率超过 15.5 元/(单·min) 时,甩单才划算(图 5)。
- 代价可以算死:最优解的总违约量 2.44 单·分钟,正好等于枚举出的下界 2.44,再压只能靠提速或加人。

图 2 单点直达时间与承诺时刻(圆=单点直达,菱形=串行后的实际到达,箭头=被推后的时间)
1.3 Gurobi 如何求解
5 单 3 点的规模可以直接枚举:16 个"甩单组合 × 访问顺序"方案,用来做基准与前沿。正式模型把顺序(弧变量 \(y_{ij}\))、是否配送(\(u_i\))、到达时刻(\(t_i\))、违约量(\(\delta_i\))放在同一个 MILP 里,软窗用"越界即罚"的方式写,保证任何输入都可行。求解墙钟 0.02 s,MIP gap 0.0e+00。
2 数学模型
2.1 符号说明
节点 \(0\) 为 12:00 的位置,\(i=1,2,3\) 为三个送达点(\(3\) 是公司超级点,代表 O3–O5 三份);\(E\) 为虚拟终点。\(y_{ij}\) 弧 \((i,j)\) 是否走;\(u_i\) 点位是否配送;\(t_i\) 到达时刻;\(\delta_i\) 软窗越界量;\(a_i,b_i\) 窗口开闭;\(s_i\) 停靠耗时;\(\tau_{ij}\) 弧上行驶时间;\(d_{ij}\) 弧长;\(\omega_i\in\{1,3\}\) 该点的订单份数;\(\pi_\ell\) 每单每分钟违约罚;\(\pi_d\) 每单甩单赔付;\(c_k\) 每公里行驶成本。
2.2 目标函数
三项分别是行驶成本、软窗违约罚、甩单赔付。\(\omega_3=3\) 是本题的关键:公司点晚 1 min 罚三次,所以模型会本能地把这个点往前排,而不是按"12:10 还早"的直觉往后放。
2.3 约束条件
式 (2)(3) 保证从 0 出发、走成一条到 \(E\) 的链,被甩的点入出度同为 0;式 (4) 是 MTZ 时间递推,兼防子回路;式 (5) 是双侧软窗,早到与晚到都算越界,等时可以把 \(t_i\) 抬到 \(a_i\),所以最优解里 \(\delta_i\) 只会由晚到触发;式 (6) 的 \(t_i\le Mu_i\) 让被甩的点不占时间轴。
2.4 规模与性质
合并后 3 个点、11 条弧,0-1 变量 16 个、约束 26 条;若把同址三单当三个独立停靠点,则 5 个点、24 条弧、30 个 0-1 变量、52 条约束——规模翻倍,而且三点之间没有弧,度约束会逼模型整组甩单。这是带软时间窗的开放式路径问题:窗只罚不封,所以永远可行,但可行域里没有任何"零违约"的点(最小违约 2.44 单·分钟),本质是区间调度不可行后的最小违约覆盖。
3 Gurobi 求解实现
3.1 建模要点
y = m.addVars(arcs, vtype=GRB.BINARY) # 弧
u = m.addVars(3, vtype=GRB.BINARY) # 点位是否配送(0=甩)
t = m.addVars(3, lb=0.0); dl = m.addVars(3, lb=0.0)
m.addConstr(t[j] >= t[i] + s[i] + TT[i,j] - M*(1 - y[i,j])) # MTZ 推进
m.addConstr(dl[k] >= t[k] - b[k]) # 晚到
m.addConstr(dl[k] >= a[k] - t[k] - M*(1 - u[k])) # 早到
m.setObjective(0.6*sum(d[i,j]*y[i,j]) + 8*sum(w[k]*dl[k])
+ 38*sum(w[k]*(1 - u[k]))) # w = (1, 1, 3)
三个实现细节:其一,同址三单折成一个超级点,权重 \(\omega=3\) 挂在目标上,0-1 变量从 30 降到 16;其二,软窗写成双向不等式,早到那一边靠"等"自消,不必单独设 0-1;其三,甩单用 \(u_i=0\) 而不删节点,删节点会让"少送一单"与"改序"的口径对不上。
3.2 求解结果
表 2 四个方案的代价拆解(元)
| 方案 | 访问顺序 | 加权违约 / 单·min | 行驶 | 违约罚 | 甩单赔付 | 合计 |
|---|---|---|---|---|---|---|
| 甲 甩掉 O2 | O1→公司(甩 O2) | 0.00 | 1.41 | 0.00 | 38.00 | 39.41 |
| 乙 甩掉 O1 | O2→公司(甩 O1) | 0.00 | 1.56 | 0.00 | 38.00 | 39.56 |
| 丙 一单不甩 | 公司→O1→O2 | 27.85 | 2.86 | 222.83 | 0.00 | 225.68 |
| 丁 改序 | O1→O2→公司 | 2.44 | 1.72 | 19.51 | 0.00 | 21.23 |
丁 改序最优:总代价 21.23 元,顺序 O1→O2→公司,只有公司那组晚 0.60 min(算 3 份,共 2.44 单·分钟),O2 晚 0.64 min。Gurobi 目标 21.2290 与 16 个方案的枚举最优 21.2290 一致到 1e-13;作为对照,把同址三单当三个独立停靠点的朴素写法算出 119.95 元(它被迫整组甩单),比正确解贵 98.72 元。

图 3 四个方案的代价分解(行驶 + 软窗违约罚 + 甩单赔付)
4 结果分析
4.1 结构与过程
图 1 里两条路线的差别不是"远近",而是谁挨罚:虚线把公司置顶(因为 12:10 听上去最晚),结果两单 12:05 全毁;实线反过来,先扫两个近点再北上,代价只是让公司那组晚 0.60 min。图 2 把矛盾画得更清楚——三个点在"单点到达 vs 承诺时刻"平面上都贴着对角线(O2 只剩 1.68 min 余量),任何串行都会把后面的点推过线。
图 4 是时间轴上的正面交锋:五条灰条是承诺窗,圆点是改序后的送达,叉是原序。改序方案只越界两处(O2 晚 0.64 min、公司那组晚 0.60 min),都在 1 min 内;原序方案则是公司早到 0.99 min、O1 晚 10.6、O2 晚 14.3 min——五单没有一张脸是好看的。

图 4 五单的承诺时间窗与两种方案的送达时刻(圆=改序,叉=原序)
4.2 灵敏度
图 5 扫违约罚率:2 元时总代价 6.60 元,21 元时 39.41 元,50 元时都是 39.41 元(已改为甩单)。15.5 元之前曲线严格线性——违约量被下界 2.44 卡死,罚率只能等比放大代价。真正的结构翻转发生在 15.5 元/(单·min):超过这个点,甩掉 O2 的 38 元赔付反而比"晚 2.44 单·分钟"便宜,阶梯从 0 跳到 1。题面给的 50 元/min 就在翻转点右侧,属于"宁可赔一单"的口径;按广州实际(超时赔付 4 元 + 差评摊销 4 元)标定到 8 元,结论是改序。
图 6 把罚率与赔付放在一张热图上:同一列(罚率相同)颜色几乎一致,同一行(赔付相同)从左到右明显变浅——罚率是主因子,赔付只在 15.5 附近决定离散跳变。星标为本案参数。
图 7 换一维:把两个 12:05 的承诺从 7 min 一路压到 1 min。承诺越紧成本越高,7 min 时只要 16.11 元,1 min 时涨到 47.06 元;3 min 是分水岭——再紧一格(2 min),O2 单点直达就要 3.32 min,最优解直接改成甩单,因为任何排序都救不回来。

图 5 违约罚率的灵敏度:最优总代价与甩单决策(双轴)

图 6 违约罚率与甩单赔付的联合影响(星=本案参数)

图 7 两个 12:05 承诺时刻的松紧灵敏度(双轴)
4.3 前沿、分布与规模
图 8 把 16 个方案摊在"违约量—总代价"平面上:非支配点只有两个,甩单落在左端(违约 0,代价 39.4),改序落在 2.44 单·分钟处(21.23 元),其余 14 个方案全被这两点支配,原序在右上角。左侧红色阴影是任何排序都到不了的区域——违约量下界 2.44 就是"三个窗打架"的定量证据。
图 9 是同一批方案的违约量直方图加累计曲线:6 个方案靠甩单把违约量压到 0,另 3 个落在 0.64–2.44 的窄带里,再往上直接跳到 9.79——中间 2–9 单·分钟是空的:离散排序下不存在"只多晚三五分钟"的折中,要么最小违约,要么赔一单。
图 10 是两种写法的规模对照:把同址三单当三个独立停靠点要 30 个 0-1 变量、52 条约束,可三点之间没有弧、度约束要求每点入出各 1,模型只能整组甩单(119.95 元);折成一个超级点后 16 个变量、26 条约束,最优 21.23 元。规模减半不是重点,不把三份餐当成三次停靠才是。

图 8 违约量与总代价的前沿及不可行区

图 9 全部方案的违约量分布与累计占比

图 10 同址三单的朴素建模与合并建模:0-1 变量与约束规模
5 结论
- 改序最优:O1→O2→公司,总代价 21.23 元(行驶 1.72 + 违约罚 19.51),只让公司那组晚 0.60 min、O2 晚 0.64 min。
- 甩一单不划算:甩 O2 要 39.41 元、甩 O1 要 39.56 元,各比改序贵 18.2 元左右;只有违约罚率超过 15.5 元/(单·min) 才翻转。
- 全部迟到代价高一个量级:原序(公司置顶)225.7 元,是改序的 10.6 倍。窗口"听上去最晚"的那一单,恰恰是最该早送的。
- 同址合并要留权重:三单折成一个超级点后 0-1 变量从 30 降到 16,但违约权重必须保持 3,否则会把三份餐的迟到当成一份,直接改变最优序。
- 落地建议:平台派单时应按 \(\omega_i/\Delta_i\)(份数除以窗口余量)排序而不是按承诺时刻排序;本案 O2 的余量只有 1.68 min,把它放在公司之后是原序失败的直接原因。违约量下界 2.44 单·分钟说明这 5 单无论怎么排都不可能全准点,只能靠增派一人或把公司组的承诺推到 12:12。
参考文献
[1] Gurobi Optimization LLC. Gurobi Optimizer Reference Manual[Z]. 2024.
[2] Kellerer H, Pferschy U, Pisinger D. Knapsack Problems[M]. Berlin: Springer, 2004.
[3] Martello S, Toth P. Knapsack Problems: Algorithms and Computer Implementations[M]. Chichester: John Wiley & Sons, 1990.
[4] 司守奎, 孙玺菁. 数学建模算法与应用[M]. 3 版. 北京: 国防工业出版社, 2021.
复现方式:python batch.py;数据与结果见同目录 .csv / .json,图见 figs/

浙公网安备 33010602011771号