案例 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 目标函数

\[\max\ \sum_{i\in I}\sum_{t\in T} w_{i}\,x_{it}\tag{1} \]

收益按单计,装满才划算——所以这个目标天然把模型推向"少趟、满装"。真正的张力全在约束里:满载省空驶,但满载也让每趟的续航变短,两边互相拉扯。

2.3 约束条件

\[e(m)=e_{0}+\kappa m,\qquad R(m)=\frac{E_{b}}{e(m)}=\frac{E_{b}}{e_{0}+\kappa m}\tag{2} \]

能耗曲线与续航曲线的定义。\(e(m)\) 是线性的,\(R(m)\) 是双曲线,这两条线是本案全部非线性的来源:前者决定"跑一公里要多少电",后者决定"这点电够跑几公里"。

\[\bar{m}_{t}=\frac{\rho\,q_{t}}{2},\qquad m_{t}=\rho\,q_{t},\qquad q_{t}=\sum_{i\in I}\sum_{l=1}^{4}p_{itl}\tag{3} \]

载重与份数的关系。\(m_{t}=\rho q_{t}\) 是起步载重(刚取完货),\(\bar{m}_{t}\) 是平均载重(沿路径线性递减,取半)。模型用后者,因为能耗是在整段路径上积分的。

\[\hat{e}_{l}=e\!\left(\frac{\rho\,\bar{Q}_{l}}{2}\right),\qquad \bar{Q}_{l}\in\{3,6,9,12\},\qquad \hat{R}_{l}=\frac{E_{b}}{\hat{e}_{l}}\tag{4} \]

四段折线:把连续载重切成 4 档,每档取右端点的能耗系数。取右端点而不是中点,是故意保守——宁可高估能耗、让方案留出余量,也不要低估以后半路趴窝。误差上界 2.2%,满载那一档恰好精确(\(\bar{m}=2.4\))。

\[\sum_{t\in T}x_{it}\le 1,\qquad \sum_{i\in I}p_{itl}\le 3\,y_{t},\qquad \sum_{l=1}^{4}p_{itl}=x_{it},\qquad \sum_{l=1}^{4}z_{tl}\le\left\lceil\frac{Q}{3}\right\rceil\tag{5} \]

分配与段位的一致性。第一式一单只能派一趟;第二式每段最多 3 份,四段满打满算 12 份;第三式每个被派的单必须落在且只落在一个段上;第四式限制每趟的段数——\(Q=4\) 时只能用到第 2 段。"逐段填满"是本题最容易被漏的一条(见 3.1)。

\[L_{t}=\sum_{i\in I}\sum_{l=1}^{4}d_{i}\,p_{itl}+D_{0}\,y_{t}\ \le\ \hat{R}_{1}-\sum_{l=2}^{4}\left(\hat{R}_{l-1}-\hat{R}_{l}\right)z_{tl}\tag{6} \]

单趟续航约束。右端是一个随段位阶梯下降的可用里程:段 1 是 51.8 公里,用到段 4 就只剩 46.9 公里。这条把"载重—续航"的非线性写成了一条线性不等式,是分段线性化真正落地的地方。

\[E=\sum_{i\in I}\sum_{t\in T}\sum_{l=1}^{4}d_{i}\,\hat{e}_{l}\,p_{itl}+D_{0}\,e_{0}\sum_{t\in T}y_{t}\ \le\ E_{b}\tag{7} \]

全天电量预算。配送段按各自段位的系数计,空驶段按空载 \(e_{0}\) 计——空驶时不载货,这是它便宜的原因,也是"多跑一趟"看起来不贵的原因。

\[\Delta=E_{b}-E,\qquad \Delta_{\mathrm{km}}=\frac{\Delta}{\bar{e}},\qquad \mu^{*}=\max\left\{\mu:\ E(\mu)\le E_{b}\right\}\tag{8} \]

三个后验量:余量 \(\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 结论

  1. 满载少趟赢了:12 份 × 2 趟,24 单全送,44.7 公里、0.910 kWh,余量 3.2 公里。 比混合方案少 0.05 kWh、少 4.0 公里,比轻装方案多送 8 单。
  2. "满载比空载少跑 7 公里"是真的,但它不是成本,是折旧。 单趟续航从 53.7 掉到 46.9 公里,可满载省下的是整趟 4 公里空驶——一趟空驶 0.073 kWh,是满载多耗 0.024 kWh 的三倍。 折算下来,满载等于用"每公里多耗 0.0007 kWh"换"少跑 4 公里空车"。
  3. 一块 1.15 kWh 电池的物理上限是 24 单(44.7 公里)。 25 单就要开第 3 趟,余量立刻转负。满载两趟的临界载重系数是 \(\mu^{*}=1.69\)——载重敏感性要再高 69.2% 才耗尽电量,说明"满载"离趴窝还有余量,真正决定上限的是单量。
  4. 轻装的代价不是电,是活。 4 份 × 4 趟跑 44.5 公里只送 16 单,每公里 0.36 单;满载是 0.54 单,高 49.3%。同样的里程,轻装只干三分之二的活。
  5. 余量 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/

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