案例 063 满载比空载少跑 7 公里
案例 063 满载比空载少跑 7 公里
摘要:12 份装满时续航从 53.7 公里掉到 46.9 公里,少跑 6.8 公里——按直觉,满载该少吃单。本文把"载重 → 能耗 → 可跑里程"这条非线性链用四段折线线性化,写成 96 个分配变量、384 个段位变量、4 个载重段、4 条出勤趟位的整数规划,在一块 1.15 kWh 电池、可用 0.977 kWh 的硬预算下比较轻装多趟、混合、满载少趟三种装法。Gurobi 的答案是满载少趟:12 份 × 2 趟,24 单全送,44.7 公里、0.910 kWh,余量 3.2 公里。混合方案同样送 24 单,却要多花 0.049 kWh、多跑 4.0 公里;轻装 4 份 × 4 趟跑满 4 趟只送得到 16 单。"满载少跑 7 公里"是真的,可它不是成本,是折旧:满载相比混合只多耗 0.024 kWh,却省下一整趟空驶的 0.073 kWh——空驶贵三倍。
关键词:载重—续航耦合;分段线性化;装载整数规划;空驶摊销;续航余量
1 问题提出
1.1 问题是什么
一辆电动车,电池 1.15 kWh,按 85% 可用放电算是 0.977 kWh。空载时每公里耗 0.0182 kWh,续航 53.7 公里;每多载 1 kg,每公里多耗 0.0011 kWh。一份外卖 0.4 kg,装满 12 份是 4.8 kg,沿路径陆续卸货,平均载重 2.4 kg,续航掉到 46.9 公里。表 1 把四种装法摆在一起。
表 1 四种装载方案的对照(24 单,一块 1.15 kWh 电池)
| 方案 | 每趟份数 | 趟数 | 送达 /单 | 总里程 /km | 全天能耗 /kWh | 续航余量 /km | 可行 |
|---|---|---|---|---|---|---|---|
| A 轻装多趟 | 4 | 4 | 16 | 44.5 | 0.848 | 6.7 | 是 |
| B 混合 | 8 | 3 | 24 | 48.7 | 0.959 | 0.9 | 是 |
| C 满载少趟 | 12 | 2 | 24 | 44.7 | 0.910 | 3.2 | 是 |
| D 六份四趟 | 6 | 4 | 24 | 52.7 | 1.008 | -1.5 | 否 |
四种装法里,A 和 C 差得最狠:A 跑了 44.5 公里只送 16 单,C 跑了 44.7 公里送 24 单——里程几乎一样(差 0.2 公里),单量差 8 单。原因不在车速,在每趟都要空车骑 4 公里回商圈取货:A 回去 4 次,C 只回去 2 次。
1.2 为什么要优化
第一,载重不是常数,是沿路径递减的变量。严格做法是给每条弧一个载重 \(l_{a}\);简化做法是按平均载重估。本例用后者,因为 12 份的卸货点均匀分布在路径上,平均载重 \(\rho q/2\) 与真实积分只差 4%。
第二,能耗和续航不是线性关系,是倒数关系。\(e(m)\) 对 \(m\) 线性,可 \(R(m)=E_{b}/e(m)\) 是双曲线:载重从 0 加到 2.4 kg,能耗涨 14.5%,续航掉 12.7%——掉的是续航,不是能耗,所以"够不够跑"这件事必须用续航算,用能耗算会高估。
第三,趟数是整数,空驶是它的影子。多跑一趟就多 4 公里空驶,这 4 公里不载货、按空载能耗计,但它固定发生,不随单量摊薄。要不要多跑一趟,取决于"省下的载重能耗"和"多出的空驶能耗"谁大——这正是整数规划要拍板的地方。
1.3 Gurobi 如何求解
模型有 \(24\times4=96\) 个订单—趟位分配 0-1 变量、\(24\times4\times4=384\) 个订单—趟位—载重段 0-1 变量、4 个趟启用变量、16 个段启用变量,共 500 个 0-1 变量、约 1 900 条约束。关键是把"载重到能耗系数"的双线性乘积 \((\sum_i d_i x_{it})\cdot e(q_t)\) 拆成"载重段"维度:第 \(l\) 段的能耗系数 \(\hat{e}_l\) 是常数,于是能耗变成 \(d_{i}\hat{e}_{l}p_{itl}\) 的三重线性求和。整题在 1 秒内证到 0.01% 间隙,三方案各解一次共 0.2 秒,余下 20 点热图用闭式解扫。闭式解与 MILP 在基准点的能耗偏差 0.000 kWh。
2 数学模型
2.1 符号说明
\(I\) 为当日 24 单,\(T=\{1,2,3,4\}\) 为出勤趟位;\(d_{i}\) 为第 \(i\) 单的单程里程(km),\(w_{i}\) 为配送费(元);\(\rho=0.40\) 为每份重量(kg);\(e_{0}=0.0182\)、\(\kappa=0.0011\) 为能耗曲线参数;\(E_{b}=0.9775\) 为可用电量(kWh);\(D_{0}=4.0\) 为每趟空驶回程(km);\(Q=12\) 为单趟份数上限;\(x_{it}\)、\(y_{t}\)、\(z_{tl}\)、\(p_{itl}\) 为 0-1 变量,分别是"单 \(i\) 派给趟 \(t\)""趟 \(t\) 启用""趟 \(t\) 用到第 \(l\) 段""单 \(i\) 落在趟 \(t\) 的第 \(l\) 段"。
2.2 目标函数
收益按单计,装满才划算——所以这个目标天然把模型推向"少趟、满装"。真正的张力全在约束里:满载省空驶,但满载也让每趟的续航变短,两边互相拉扯。
2.3 约束条件
能耗曲线与续航曲线的定义。\(e(m)\) 是线性的,\(R(m)\) 是双曲线,这两条线是本案全部非线性的来源:前者决定"跑一公里要多少电",后者决定"这点电够跑几公里"。
载重与份数的关系。\(m_{t}=\rho q_{t}\) 是起步载重(刚取完货),\(\bar{m}_{t}\) 是平均载重(沿路径线性递减,取半)。模型用后者,因为能耗是在整段路径上积分的。
四段折线:把连续载重切成 4 档,每档取右端点的能耗系数。取右端点而不是中点,是故意保守——宁可高估能耗、让方案留出余量,也不要低估以后半路趴窝。误差上界 2.2%,满载那一档恰好精确(\(\bar{m}=2.4\))。
分配与段位的一致性。第一式一单只能派一趟;第二式每段最多 3 份,四段满打满算 12 份;第三式每个被派的单必须落在且只落在一个段上;第四式限制每趟的段数——\(Q=4\) 时只能用到第 2 段。"逐段填满"是本题最容易被漏的一条(见 3.1)。
单趟续航约束。右端是一个随段位阶梯下降的可用里程:段 1 是 51.8 公里,用到段 4 就只剩 46.9 公里。这条把"载重—续航"的非线性写成了一条线性不等式,是分段线性化真正落地的地方。
全天电量预算。配送段按各自段位的系数计,空驶段按空载 \(e_{0}\) 计——空驶时不载货,这是它便宜的原因,也是"多跑一趟"看起来不贵的原因。
三个后验量:余量 \(\Delta\)(kWh)、折算成公里的余量 \(\Delta_{\mathrm{km}}\)、以及临界载重系数 \(\mu^{*}\)——把 \(\kappa\) 乘上 \(\mu\) 后,使方案刚好不可行的最大 \(\mu\)。
2.4 规模与性质
500 个 0-1 变量、约 1 900 条约束,规模不大,但解不能靠"每公里成本乘里程"推出来:能耗系数依赖份数,份数依赖趟数,趟数又决定空驶次数,三者绕成一个环。更麻烦的是它是一个带分段线性约束的多背包问题——每趟是一个容量 12 的背包,背包的"容量"本身还随装多少而变。
3 Gurobi 求解实现
3.1 建模要点
# 四折线段:按份数分档,段内取右端点的能耗(保守上界)
SEGQ = [3, 6, 9, 12]
eh = [E0C + KAP*(RHO*q/2.0) for q in SEGQ] # 0.01886 ... 0.02084
rg = [EB/e for e in eh] # 51.8 ... 46.9 km
pp = m.addVars(N, T, 4, vtype=GRB.BINARY) # 单 i 在趟 t 的第 l 段
zs = m.addVars(T, 4, vtype=GRB.BINARY) # 趟 t 用到第 l 段
for t in range(T):
for l in range(4):
m.addConstr(gp.quicksum(pp[i,t,l] for i in range(N)) <= 3*y[t]) # 段容量 3 份
m.addConstr(zs[t,l] <= gp.quicksum(pp[i,t,l] for i in range(N))) # 段启用
if l < 3: # 逐段填满
m.addConstr(gp.quicksum(pp[i,t,l+1] for i in range(N))
<= gp.quicksum(pp[i,t,l] for i in range(N)))
# 单趟续航:载得越满,可用里程越短(阶梯下降)
m.addConstr(gp.quicksum(d[i]*pp[i,t,l] for i in range(N) for l in range(4)) + D0*y[t]
<= rg[0] - gp.quicksum((rg[l-1]-rg[l])*zs[t,l] for l in range(1,4)))
# 全天电量预算:配送段 + 空驶回程
m.addConstr(gp.quicksum(d[i]*eh[l]*pp[i,t,l] for i in range(N) for t in range(T)
for l in range(4)) + D0*E0C*gp.quicksum(y) <= EB)
两个实现细节。第一,"逐段填满"那条约束不能省:若不加 \(\sum_i p_{i,t,l+1}\le\sum_i p_{i,t,l}\),模型会跳着装——把 4 个单塞进第 4 段,用 \(\hat{e}_{4}\) 的系数只算 4 个单的能耗,却让 \(z_{t4}=1\) 把续航压到 46.9 公里,看起来"满足"了续航约束,实际上段位和份数脱节,解是假的。第二,趟启用变量要加单调性 \(y_{t}\ge y_{t+1}\):4 个趟位完全对称,不破对称会多出 \(4!=24\) 倍的等价解,分支定界在对称子树上白跑,求解时间从 0.3 秒涨到 20 秒以上。
3.2 求解结果
表 2 载重—续航的四段折线(可用电量 0.9775 kWh)
| 折线段 | 份数区间 | 平均载重 /kg | 能耗 /(kWh/km) | 续航 /km | 备注 |
|---|---|---|---|---|---|
| 1 | 1—3 | 0.6 | 0.0189 | 51.8 | 轻装 |
| 2 | 4—6 | 1.2 | 0.0195 | 50.1 | 半载 |
| 3 | 7—9 | 1.8 | 0.0202 | 48.4 | 重载 |
| 4 | 10—12 | 2.4 | 0.0208 | 46.9 | 满载 |
表 2 是折线本身:四段的能耗从 0.0189 爬到 0.0208,涨 10.5%;续航从 51.8 掉到 46.9,掉 4.9 公里。 这四段只覆盖 1 到 12 份;若从空载算起,续航 53.7,到满载 46.9,落差 6.8 公里——与题面 55→48 的比例完全一致:48 除以 55 是 0.873,46.9 除以 53.7 是 0.873,差在千分位以内。
表 3 日单量扫描:最优趟数与续航余量
| 日单量 /单 | 最优趟数 | 每趟份数 | 总里程 /km | 全天能耗 /kWh | 余量 /km |
|---|---|---|---|---|---|
| 12 | 1 | 12 | 26.7 | 0.546 | 20.7 |
| 16 | 2 | 8 | 36.5 | 0.714 | 13.2 |
| 20 | 2 | 10 | 41.4 | 0.827 | 7.4 |
| 24 | 2 | 12 | 44.7 | 0.910 | 3.2 |
| 28 | 3 | 10 | 52.1 | 1.036 | -2.9 |
| 32 | 3 | 11 | 54.1 | 1.087 | -5.3 |
4 结果分析

图 1 载重—续航曲线与四段折线近似(横轴为平均载重)
**4.1 续航是一条双曲线,不是一条直线** 图 1 蓝线是 $R(m)$:平均载重 0 时 53.7 公里,2.4 kg 时 46.9 公里,落差 6.8 公里。**橙虚线是四段折线,四段的端点分别是 0.0, 0.6, 1.2, 1.8, 2.4 kg,对应的续航 53.7, 51.8, 50.1, 48.4, 46.9 公里。** 折线与曲线几乎重合,因为取的是右端点——**段内高估、段间不连续,正好形成一个"永远偏保守"的阶梯。** 注意曲线的形状:载重 0 到 2.4 kg 掉了 12.7%,2.4 到 4.8 kg 又要掉 13%——**掉得越来越快,因为双曲线的分母是线性的,倒数却是加速的。**
图 2 24 单的里程—配送费散点(气泡=单均能耗,灰点=备选池)
**4.2 单子越远,单位能耗越高,收益却只涨一点点** 图 2 横轴单程里程、纵轴配送费。**配送费是 $4.0+0.62d$,斜率 0.62 元每公里,线性的;能耗是 $d\cdot e(\bar{m})$,也是线性的。** 但气泡的橙色深浅说明:最远那单(2.40 km)能耗是最短那单(0.60 km)的 4.0 倍,收益只差 1.1 元。**这就是为什么"多带单"比"挑远单"划算:单量决定空驶摊销,里程只决定边际成本。**
图 3 三种装载方案的全天能耗(柱内=能耗,柱下=里程与送达单量)
**4.3 满载方案的能耗最低,不是最高** 图 3 三根柱:A 轻装 0.848 kWh、B 混合 0.959 kWh、C 满载 0.910 kWh,红线是 0.977 的预算。**直觉认为满载因为"每公里更耗电"所以总能耗更高,结果是反的——C 比 B 少 0.049 kWh。** 原因拆开看:C 的配送段系数 0.0208 比 B 的 0.0202 高 3.3%,36.7 公里多耗 0.024 kWh;**可 C 只回 2 趟、B 回 3 趟,省下的那一趟空驶是 0.073 kWh——净赚 0.049 kWh,是"多耗"的三倍。** 空驶赢,而且赢得不靠运气。
图 4 三种方案的累计里程阶梯(横轴为出勤趟序)
**4.4 轻装跑一样远,只送到三分之二** 图 4 是累计里程。**A 的阶梯爬 4 级到 44.5 公里,C 爬 2 级到 44.7 公里——终点几乎一样(差 0.2 公里),但 A 每级只装 4 单。** 换算成效率:C 每公里送 0.54 单,A 只有 0.36 单,**差 49.3%。** 这条阶梯说明:轻装的代价不是"多耗电",是"同样的路只干三分之二的活"。
图 5 日单量对续航余量与最优趟数的影响(右轴为趟数阶梯)
**4.5 主灵敏度:24 单就是这块电池的天花板** 图 5 左轴余量、右轴最优趟数。**12 单时 1 趟装完,余量 20.7 公里;20 单还是 2 趟,余量还剩 7.4 公里;24 单 2 趟,余量 3.2 公里;到 28 单,2 趟装不下(上限 24 份),只能开 3 趟,余量掉到 -2.9 公里——已经是缺口。** 红色阴影是"余量为负"的跑不完区。注意右轴阶梯:**趟数从 1 跳到 2 在 13 单,从 2 跳到 3 在 25 单,每跳一次余量就断崖式下跌 4.2 公里左右**——因为跳一次就多 4 公里空驶,还要把满载系数降一档。
图 6 日单量 × 载重系数的续航余量热图(星标为本案参数)
**4.6 二维灵敏度:载重系数是"斜着切"的一刀** 图 6 是 4 档单量乘 5 档载重系数,格子里是余量公里数。**沿载重系数方向看 24 单那一列:μ 从 0.30 涨到 1.90,余量从 7.1 一路滑到 -0.9,临界落在 μ*=1.69——载重敏感性要再高 69.2%,满载两趟才真的跑不完。** 沿单量方向看 μ=1.00 那一行:16 单 13.2 → 20 单 7.4 → 24 单 3.2 → 28 单 -2.9,**16 到 20 单掉 5.8 公里,20 到 24 单掉 4.2 公里,24 到 28 单掉 6.1 公里。** 两条路都能把余量吃干:沿载重方向(24 单那一列)走满 5 档掉 8.0 公里,沿单量方向(μ=1.00 那一行)走满 4 档掉 16.1 公里——**但载重系数是车的属性、改不了,单量是骑手每天要拍板的那一个。**
图 7 每份重量对续航落差与满载方案余量的影响(双轴)
**4.7 第三维:一份多重,比一份几个更要命** 图 7 横轴每份重量,左轴空载与满载的续航落差,右轴满载方案的余量。**份重从 0.2 kg 涨到 0.6 kg,落差从 3.6 公里涨到 9.6 公里,翻了一倍多;余量从 5.9 公里掉到 0.8 公里。** 本案的 0.40 kg 处在中间偏右。**这条线解释了为什么奶茶单和整箱水单不能按"份数"混着算:份数是装载单位,重量才是能耗单位。**
图 8 能耗—送达单量的可行前沿,灰区为超预算不可行区
**4.8 前沿:能送 24 单的只有两趟** 图 8 横轴全天能耗、纵轴送达单量,四组点按趟数分色。**1 趟方案(紫)最多 12 单,2 趟(蓝)最多 24,3 趟(橙)能到 30 但能耗已经越线,4 趟(灰)一出发就不可行。** 红色虚线是可行前沿:**1 趟最多 12 单(紫点爬到 0.55 kWh 就停),2 趟能到 24 单(0.910 kWh),3 趟虽然也能到 24 单,却要 0.959 kWh,比 2 趟多烧 0.05 kWh 换同样的单量——它不在前沿上,是被支配的那一类。** 星号落在最右下的可行角上:**再往右一格就是阴影区,24 单是这块电池的物理上限。**
图 9 日里程随机波动下满载方案的续航余量分布与累计概率
**4.9 余量只有 3.2 公里,波动 10% 就有 18.7% 的日子趴窝** 图 9 把单均里程按 ±10% 抽 4000 天。**余量的分布中心在 3.3 公里,标准差 3.7 公里,落在 0 左边的概率是 18.7%。** 换句话说:**方案 C 在"平均的一天"里余 3.2 公里,但每 5 天就有一天要推车。** 这就是"满载少趟"真正的代价——不是多花钱,是把容错空间花掉了。若把余量要求提到 5 公里(保守骑手),可行单量就得从 24 降到 22。
图 10 同一批 24 单在不同趟数下的能耗分解(深色=带载,浅色=空驶)
**4.10 边际贡献:每一趟空驶都是固定成本** 图 10 是 1 到 4 趟的堆叠。**深色那块是带载能耗,从 1 趟的 0.473 kWh 涨到 4 趟的 0.716 kWh——涨得慢,因为趟数越多每趟载得越轻;浅色那块是空驶,从 0.073 kWh 翻到 0.291 kWh,是纯增量。** 合计从 0.546(只送 12 单)爬到 1.008(送 24 单),**第 4 趟那根柱子已经越过 0.977 的红线。** 注意 2 趟和 3 趟的深色几乎一样(0.765 对 0.733),**差的全在浅色——多一趟就是多 0.073 kWh,一分不少。**5 结论
- 满载少趟赢了:12 份 × 2 趟,24 单全送,44.7 公里、0.910 kWh,余量 3.2 公里。 比混合方案少 0.05 kWh、少 4.0 公里,比轻装方案多送 8 单。
- "满载比空载少跑 7 公里"是真的,但它不是成本,是折旧。 单趟续航从 53.7 掉到 46.9 公里,可满载省下的是整趟 4 公里空驶——一趟空驶 0.073 kWh,是满载多耗 0.024 kWh 的三倍。 折算下来,满载等于用"每公里多耗 0.0007 kWh"换"少跑 4 公里空车"。
- 一块 1.15 kWh 电池的物理上限是 24 单(44.7 公里)。 25 单就要开第 3 趟,余量立刻转负。满载两趟的临界载重系数是 \(\mu^{*}=1.69\)——载重敏感性要再高 69.2% 才耗尽电量,说明"满载"离趴窝还有余量,真正决定上限的是单量。
- 轻装的代价不是电,是活。 4 份 × 4 趟跑 44.5 公里只送 16 单,每公里 0.36 单;满载是 0.54 单,高 49.3%。同样的里程,轻装只干三分之二的活。
- 余量 3.2 公里意味着 18.7% 的趴窝概率。 满载方案的脆点不在钱上,在容错上:里程涨 10% 就推车。要稳,单量就得从 24 退到 22——这是"满载划算"唯一的对价。
参考文献
[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 -u batch_063.py;数据与结果见同目录 data_063.csv / results_063.json,图见 figs/

浙公网安备 33010602011771号