案例 048 急件插进来:在途订单被急件插入的滚动重排

案例 048 急件插进来:在途订单被急件插入的滚动重排

摘要:外卖小哥手上正跑着 6 单,原路线 88 min、23.2 km,6 单全部按承诺时刻留有余量;闪送此时弹来一张 18 元、15 min 必达的急件。本文把"插不插、插在哪"写成 7 个插入位置 + 1 个接单指示的 0-1 规划,并另建一个 7 单全重排的时序模型作对照。结果:7 个位置里只有"第一站之前"一个能赶上 15 min 死线,代价是总时长增加 6.1 min、6 张在途单全部超时;而最便宜的位置(第 2 位,只多 0.7 min、零超时)急件要到 22.9 min 才送达,超死线 7.9 min。四方案实测:原序尾插 48.94 元、强行插队 50.83 元、全局重排 77.64 元、拒接 57.50 元——硬插比拒接还少赚 6.7 元,重排却多赚 20.1 元。临界溢价:硬插需 24.7 元,重排为 -2.1 元(重排省下的 9.4 min 已把急件成本全部抵掉)。

关键词:滚动重排;插入位置;时间窗;0-1 规划;临界溢价;超时传导

1 问题提出

1.1 问题是什么

午高峰的尾巴,小哥车停在体育西路口,车上挂着 6 张已接单。按原计划跑完要 88 min、23.2 km,均速 19.9 km/h;每单到达时刻都在承诺时限之前,余量 2—5 min。第 7 张单在选择的那一刻弹进来:取餐在天河村、送杨箕,18 元,15 min 内必达。

表 1 在途 6 单与急件:承诺时刻一律按「预算到达 + 余量」给出(时刻单位 min,自决策点起算)

订单 类型 取餐→送达 收入 元 服务 min 计划到达 承诺时限 余量 尾插 插队 重排
01 在途 石牌村→珠江新城 12.0 3 16.5 19.5 3 是 否 否
02 在途 岗顶→天河北 9.5 3 26.2 28.2 2 是 否 是
03 在途 天河南→体育中心 8.0 3 35.5 39.5 4 是 否 是
04 在途 珠江新城西→员村 10.5 3 61.3 64.3 3 是 否 是
05 在途 五羊新城→客村 6.5 3 74.9 79.9 5 是 否 是
06 在途 赤岗北→赤岗 11.0 3 88.0 92.0 4 是 否 是
07 急件 天河村→杨箕 18.0 2 — 15.0 — 否 是 是

表 1 的三列"尾插 / 插队 / 重排"是同一张单在三种跑法下能否按时:尾插把急件排在最后,急件 126.4 min 才到;插队把它塞在第一站之前,急件 9.8 min 到、6 张在途单全崩;重排把 7 单一起重排,急件 9.8 min 到、只有 1 单超时。

1.2 为什么要优化

第一,位置不是随便挑的。插入位置 \(k\) 有两个互相打架的后果:越靠前,急件越可能赶上死线;越靠后,已接单被推移得越少。本例最省时的位置(\(k=1\))只多 0.7 min、一张都不超时,可急件要到 22.9 min 才送到,死线早就过了。

第二,超时是成串传导的。插在第一站之前,后面 6 单的到达时刻被整体推移 6.1 min,而它们的余量只有 2—5 min,于是一张不剩地全部超时。超时不是"这一单的事",是"整条链的事"。

第三,拒接也是一种方案,而且不差。接急件要付 18 元的对价,却要赔 6 单超时;本例硬插之后净收益 50.83 元,比什么都不做(57.50 元)还低 6.7 元。"多接一单一定多赚"是错觉。

1.3 Gurobi 如何求解

两套模型:插入式是 7 个位置变量 \(z_{k}\) 加 1 个接单变量 \(u\),规模 8 个二进制;重排式是 7 节点带时间窗的开路排序,用弧变量 \(x_{ij}\) 加到达时刻 \(\tau_{i}\) 的时序约束,规模 56 个二进制。两者都在 0.1 s 内证到最优(MIPGap \(10^{-9}\))。真正花时间的是预计算:每个插入位置对后续 6 单到达时刻的推移量,以及 5040 种全排列的枚举对照。

2 数学模型

2.1 符号说明

\(i=1,\dots,6\) 为在途单,\(i=7\) 为急件;\(w_{i}\) 为收入,\(s_{i}\) 为服务时长,\(\bar t_{i}\) 为承诺送达时刻,\(T_{0}=88\) 为原路线总时长;\(k=0,\dots,6\) 为插入位置(\(k\) 表示插在第 \(k+1\) 单之前,\(k=6\) 为队尾)。预计算量:\(\Delta_{k}\) 为在位置 \(k\) 插入后总时长的增量,\(a_{k}\) 为急件在该位置的送达时刻,\(n_{k}\) 为因此新增的超时单数。决策变量 \(z_{k}\in\{0,1\}\) 选位置,\(u\in\{0,1\}\) 是否接单。

2.2 目标函数

\[\max\ Z=p\,u-\sum_{k=0}^{6}\big(c_{d}\Delta_{k}+c_{t}n_{k}\big)z_{k}\tag{1} \]

\(p\) 为急件单价,\(c_{d}=0.6\) 元/min 是时长成本,\(c_{t}=3.5\) 元/单是每张超时单的赔付加口碑折价。式 (1) 只对新插入的动作计价,6 单的基础收益是常数,可以不进目标。

2.3 约束条件

位置与接单一一对应,不接就一个位置也不选:

\[\sum_{k=0}^{6}z_{k}=u\tag{2} \]

总时长不能超过 110 min,急件必须赶上死线:

\[T_{0}+\sum_{k=0}^{6}\Delta_{k}z_{k}\le 110,\qquad \sum_{k=0}^{6}a_{k}z_{k}\le 15\tag{3} \]

重排模型把顺序也放开,用弧变量与到达时刻刻画:每点一进一出,且

\[\tau_{j}\ge \tau_{i}+s_{i}+t_{ij}-M(1-x_{ij}),\qquad \tau_{7}\le 15,\qquad \tau_{E}\le 110\tag{4} \]

超时用指示变量 \(\ell_{i}\) 计数,\(\tau_{i}-\bar t_{i}\le M\ell_{i}\),重排目标为 \(\min\ c_{d}\tau_{E}+c_{t}\sum_{i}\ell_{i}\)。原序尾插方案不必建模,直接把急件接到队尾即可;违约赔付按急件超时超过 20 min 时赔 60% 单价计。

\[z_{k},\,u,\,x_{ij},\,\ell_{i}\in\{0,1\}\tag{5} \]

2.4 规模与性质

插入模型 8 个二进制、3 条耦合约束,本质是"在 7 个候选里挑 1 个",背包式结构,分支定界一步收敛。重排模型 7 个节点、57 个弧变量、7 个时间变量,是带时间窗的开路 TSP,规模小但时间约束密。两个模型的难度都不在求解,而在预计算 \(\Delta_{k}\) 与 \(n_{k}\)——题源说得很准:工作量全在"插入位置对后续到达时刻的推移量"。

3 Gurobi 求解实现

3.1 建模要点

插入位置只有 7 个,先把每个位置的后果算成常数,模型里就只剩线性项:

for k in range(N + 1):
    seq = list(range(k)) + [6] + list(range(k, N))
    arr, T = route_eval(seq)
    DT[k] = T - T0
    NL[k] = sum(1 for i in range(N) if arr[i] > DDL[i])
    AU[k] = arr[6]
m.setObjective(p * u - gp.quicksum((CD * DT[k] + CP * NL[k]) * z[k] for k in range(N + 1)),
               GRB.MAXIMIZE)
m.addConstr(gp.quicksum(z[k] for k in range(N + 1)) == u)
m.addConstr(gp.quicksum(AU[k] * z[k] for k in range(N + 1)) <= theta)   # 死线

一个实现细节:不可行的位置要显式置零(z[k] == 0),而不是靠大 \(M\) 把它挤掉。本例 7 个位置里 6 个违反死线,若不置零,约束 \(a_{k}\le 15\) 会把模型逼成不可行,而不是给出"不接"的答案——接不接是决策变量 \(u\) 的事,位置可行性是数据的事,两者必须分开写。

3.2 求解结果

表 2 四种方案的时长、超时与净收益(金额单位:元)

方案 超时单数 总时长 min 时长成本 超时赔付 违约赔付 净收益
A 原序尾插 1 126.4 23.1 3.5 10.8 48.94
B 强行插队 6 94.1 3.7 21.0 0.0 50.83
C 全局重排 1 78.6 -5.6 3.5 0.0 77.64
D 拒接急件 0 88.0 0.0 0.0 0.0 57.50

四方案的排序是 C(77.64)> D(57.50)> B(50.83)> A(48.94)。最反直觉的一条:B 强行插队比 D 拒接还差 6.7 元,因为 6 单超时要赔 21.0 元,而急件只带来 18 元。

4 结果分析

图 1 重排方案的实际时序(急件卡死线、06 反而提前收工)

4.1 重排后的时序 图 1 是重排方案的实际时刻表:急件卡在 9.8 min 送达(死线 15 min),之后 02、03、01、04、05 依次落地,只有 01 一单越过了自己的承诺线(红色),06 收在 78.6 min,比原计划还早 9.4 min。重排不是把急件"塞进去",而是把整条链重新捋直——捋直之后多出来的 9.4 min 正好给急件让路。

图 2 七张单的收入—占用时长散点(气泡为承诺余量)

4.2 单量结构 图 2 横轴是单均占用时长、纵轴是收入、气泡是承诺余量。急件(橙)收入 18 元最高、占用只有 9 min,看着是最划算的一张;但它的气泡最小——余量为零,一秒钟都拖不起。6 张在途单收入 6.5—12.0 元、占用 9—26 min,余量 2—5 min,全是"薄利且紧"的单:它们的价值不在单笔高低,而在已经排好的顺序里。

图 3 四种跑法的净收益与超时单数

4.3 四方案对比 图 3 把四种跑法摆在一起。A 尾插 48.94 元最差:急件 126.4 min 才送到、迟到 111.4 min,触发触发 60% 违约赔付(18 元);B 硬插 50.83 元,急件赶上了,6 张在途单全超时;D 拒接 57.50 元,什么都不赔;C 重排 77.64 元,急件按时、只赔 1 单。A < B < D < C 这个次序说明:接不接不是关键,怎么接才是。

图 4 按送达时刻累计的毛收入

4.4 累计视角 图 4 按送达时刻累计毛收入。A 尾插在前 88 min 里和老路线完全重合,最后才靠急件把收入抬到 75.5 元附近,但那时已经违约;B 硬插在开头就拿到急件,曲线起步快,可 6 单全压在承诺线之后;C 重排的曲线最陡——同样的 7 张单,它在 78.6 min 内全部跑完,比 A 早 48 min 收工。收入曲线陡不陡,取决于单位时间里能落下几单,而不是单笔多大。

图 5 急件单价灵敏度:净收益与接单决策

4.5 主灵敏度:急件溢价 图 5 让单价从 8 元扫到 28 元。两条策略线斜率都是 +1(每多一元全进兜里),但它们与"拒接"水平线 57.50 元的交点完全不同:硬插的交点在 24.7 元,重排的交点在 -2.1 元。18 元的急件,硬插只值 50.83 元(低于拒接),重排却值 77.64 元。右轴的接单决策阶梯也印证:硬插要等到 24.7 元以上才该接,重排则全程为 1。

图 6 急件单价 × 超时赔付的净收益热图

4.6 二维:单价与超时赔付 图 6 是 4 行 5 列的净收益热图。单价沿横向每抬 1 元,净收益就抬 1 元,整行整行地平移;超时赔付沿纵向每加 1 元,净收益只掉 1 元——因为重排后只剩 1 单超时。全图从 66.1 元到 89.1 元,落差全部由这两个参数线性叠加而成。赔付系数在这里只是"平移项",不是"开关项":重排已经把超时压到 1 单,再怎么调赔付也翻不了盘。

图 7 承诺余量松紧灵敏度:净收益与超时单数

4.7 第三维:承诺余量松紧 图 7 把 6 单的承诺余量整体乘以 0.7 到 1.3 的系数。余量压到 0.7 倍时,重排后仍有 4 单超时,净收益跌到 67.1 元;余量放宽到 1.3 倍时降到 1 单,净收益回到 77.6 元,并在这里走平——再宽也不涨了,因为剩下的瓶颈是总时长 110 min,不是承诺线。余量系数在 0.9 以上就进入平台区,说明本案的 2—5 min 余量已经偏紧,正是它把 6 单一起推下了水。

图 8 5040 种排列的总时长—净收益分布与帕累托前沿

4.8 帕累托前沿与不可行域 图 8 把 5040 种排列全画出来,横轴总时长、纵轴净收益,灰区是超过 110 min 的部分。5040 种排法里,急件能按时且总时长不超限的只有 13 种,占 0.26%;这 13 种全部落在图左上角那条窄带里,前沿只有 1 个点——就是重排解(77.64 元、78.6 min)。A 尾插(倒三角)掉进灰区深处,B 硬插(方块)在可行域内但远离前沿。可行域窄到这个程度,说明"随便插一个位置"根本不是选项,只有求解器能给答案。

图 9 全部排列的净收益分布与四方案位置

4.9 净收益分布 图 9 是 5040 种排列的净收益直方图,深色是那 13 种可行排法。全部排列的中位数 53.2 元,可行排法的上限 77.6 元。四条竖线里,C 站在最右端(可行域的最大值),D 在中间,B 和 A 落在左侧主体里——四方案的差距不是"运气差一点",而是跨了一个分布的主体。

图 10 四种跑法的净收益结构分解

4.10 净收益分解 图 10 把四个方案拆成"在途收入 + 急件收入 − 时长成本 − 超时赔付 − 违约赔付"。A 的违约赔付 18 元砍掉一整块;B 的急件收入 18 元被 21.0 元的超时赔付吃掉还有余;C 的时长成本是负的(省了 5.6 元),所以柱子比 6 单毛收入还高;D 什么都不加不减,就是 57.50 元。看这张图就知道:急件的账要三笔一起算——收入、时长、赔付,缺一笔都会得出相反的结论。

5 结论

  1. 7 个插入位置里只有 1 个能满足 15 min 死线,代价是总时长 +6.1 min、6 张在途单全部超时。死线越紧,位置自由度越小;本例的自由度是 1。

  2. 最便宜的位置(第 2 位,只多 0.7 min、零超时)恰恰不可行:急件要到 22.9 min 才送到。"代价最小"和"赶得上"是两件事,必须先做可行性筛选再谈优化。

  3. 四种跑法排序 C > D > B > A:重排 77.64 元 > 拒接 57.50 元 > 硬插 50.83 元 > 尾插 48.94 元。硬插比拒接还少赚 6.7 元,因为 6 单超时赔付 21.0 元超过了急件的 18 元。

  4. 临界溢价可以精确算:硬插要 \(p\ge 24.7\) 元才保本,重排要 \(p\ge -2.1\) 元——后者是负数,意思是重排省下的 9.4 min(折 5.6 元)已经把急件的全部代价抵掉。先重排,再谈溢价;顺序反了,结论就反了。

  5. 5040 种排法里只有 13 种可行(0.26%),可行域在"总时长—净收益"平面上是一条窄带,前沿只有 1 个点。这不是靠经验能试出来的问题,8 个变量的 0-1 规划 0.1 s 就能给出确定答案。

参考文献

[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_048.py;数据与结果见同目录 .csv / .json,图见 figs/

posted @ 2026-11-11 18:02  小青蛙电气  阅读(2)  评论(0)    收藏  举报