ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

python的运筹学工业场景模拟第四十四篇:快递中转仓,多批次货物转运,中转仓容量限制,构建运输模型,求解转运分配。

python的运筹学工业场景模拟第四十四篇:快递中转仓,多批次货物转运,中转仓容量限制,构建运输模型,求解转运分配。 快递中转仓多批次转运分配用转运问题把爆仓变成精准调度某电商物流区域分拨中心每天要处理6个揽收点→3个中转仓→5个配送站的两级转运。早上6点~10点是到货高峰3个中转仓的暂存面积瞬间被打满——仓1设计容量500㎡但早高峰同时到了3批货堆了680㎡直接爆仓。现场调度员靠对讲机喊仓1放不下了这批改送仓2——结果仓2也满了最后临时租了200㎡的场外帐篷一天租金8000元二次搬运费1.2万。后来我用带中转仓容量约束的转运问题线性规划建模把未来4个小时的到货批次全部预排PuLP跑了0.05秒给出了转运分配方案仓1放420㎡、仓2放380㎡、仓3放350㎡全部不超容零爆仓。单日转运成本从3.8万降到2.4万省1.4万一年省510万。—— 参考北京理工大学《运筹学》第7章运输与分配问题、第2章线性规划一、实际应用场景描述快递中转仓多批次转运分配Transshipment Problem with Transshipment Node Capacity是物流网络中的经典问题。凡是多个源点→多个中转节点→多个目的地的两级/多级网络都是它行业 源点 中转仓 目的地 约束电商物流 揽收点/商家仓 区域分拨中心 末端配送站 中转仓面积/分拣线能力快消品分销 工厂 区域DC 经销商/门店 中转仓库容、装卸月台医药流通 药厂 医药商业仓 医院/药店 GSP仓容、温控区汽车零部件JIT 供应商 排序中心 总装线边 排序区面积、节拍生鲜冷链 产地冷库 销地中转冷库 商超/前置仓 冷库容积、保质期3C电子 代工厂 HUB仓 区域售后 仓容、SN管理核心矛盾揽收点要把货发到配送站但不能直达或直达太贵——必须经过中转仓分拣。中转仓有面积上限和分拣能力上限早高峰多批次同时到货会瞬间打满。要在总转运成本最低和中转仓不爆仓之间找全局最优。┌──────────────────────────────────────────────────────────────┐│ 快递中转仓多批次转运分配优化系统 · 转运问题建模 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 源点(揽收点): S1~S6, 各批次到货量已知(㎡体积) │││ │ 中转仓: W1~W3, 各有面积上限(㎡) │││ │ 目的地(配送站): D1~D5, 各站收货需求已知(㎡) │││ │ │││ │ 成本参数: │││ │ • 揽收点→中转仓: 短驳运费(元/㎡) ││ │ • 中转仓→配送站: 支线配送费(元/㎡) │││ │ • 中转仓操作费: 入库分拣出库(元/㎡) │││ │ │││ │ 约束: │││ │ • 每个中转仓总流入量 ≤ 面积上限 ││ │ • 每个揽收点出货量 到货量 ││ │ • 每个配送站接收量 需求量 ││ │ • 中转仓流量守恒: 流入 流出 │││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 ││ • 成本: 就近中转→运费低 ││ • 容量: 早高峰多批同时到→中转仓爆仓 ││ • 流量: 中转仓不能只进不出→必须及时分拣出库 ││ ││ 【本程序处理流程】 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐││ │ 到货批次 │──►│ 仓容 │──►│ 线性规划 │──►│ 转运分配 │││ │ 需求 │ │ 成本矩阵 │ │ (PuLP) │ │ 方案 │││ └──────────┘ └──────────┘ └──────────┘ └──────────┘││ ││ 【输出结果】 ││ • 每个批次→中转仓→配送站的完整转运路径和数量 ││ • 各中转仓流入/流出/占用面积 ││ • 总成本分解(短驳操作配送) ││ • 与就近人工调度方案的量化对比 │└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某电商物流区域分拨中心调度主管原话我们这片区有6个揽收点就是快递员把货收上来集中的地方3个中转仓W1在城北、W2在城南、W3在城东5个配送站覆盖全城。每天早6点~10点是到货高峰。6个揽收点陆陆续续把货拉过来——每批货的体积我提前都知道系统里有预估。但我只有3个中转仓每个仓的暂存区面积有限W1只有500㎡、W2有450㎡、W3有400㎡。我的调度逻辑是就近先到先分S1离W1近→全送W1S2离W2近→全送W2。结果7点半的时候S1、S3、S5三批货同时到了W1——堆了680㎡爆了180㎡我赶紧用对讲机喊后面的货改去W2——但W2也满了。最后没办法临时租了200㎡的场外帐篷一天租金8000块还要把货从帐篷二次搬到W1里分拣搬运费又花了1.2万。其实我提前一天晚上就知道第二天早上的到货量了——但我没有工具去预排。Excel里我试着排过一次6个源点×3个仓×5个站90个变量我排了3个小时最后发现W3的货分配不均——有的站收不到。后来IT组的小伙用Python跑了个线性规划——0.05秒给出方案S1的货分两拨60%去W1、40%去W3S3全去W2S5分三拨…… 每个仓的流入量都卡在面积上限以内零爆仓。单日转运成本从3.8万降到2.4万——一天省1.4万一年就是510万。2.2 人工就近调度 vs 线性规划优化量化对比指标 人工就近调度 线性规划本方案 改善效果方案耗时 3 小时还爆仓了 0.05 秒 -99.9%单日转运总成本 3.8 万元 2.4 万元 -36.8%爆仓次数 日均1~2次租帐篷二次搬运 0 次 消除中转仓面积利用率 W1爆仓、W3只用了60% 全部80%~95% 均衡年化价值 - 约 510 万元/年 综合关键发现人工调度的本质是贪心就近——但它完全忽略了各仓的剩余面积在不同时间片不同。线性规划把所有批次×所有仓×所有站的全局流量一起算自动把货流分散到各仓让每个仓的流入量刚好卡在面积上限附近但不超——这是人脑在90个变量面前不可能做到的。2.3 核心矛盾中转仓转运分配的核心矛盾是就近便宜与仓容有限之间的博弈——再加上中转仓必须流入流出的流量守恒。这不是简单的运输问题——是带中转节点的转运问题Transshipment Problem。中转仓既是目的地从揽收点收货又是源点向配送站发货。要在两级运输成本中转操作成本之和最小的前提下让每个中转仓不爆仓。三、核心逻辑讲解大白话版3.1 用大白话解释中转仓转运分配想象你在组织一场接力传球比赛场景- 有6个发球手揽收点每人手里有不同数量的球货。- 有3个中转站中转仓每个中转站地面大小有限面积上限——球堆在地上地满了就放不下。- 有5个接球手配送站每人需要接到特定数量的球。- 规则1. 发球手不能直接把球扔给接球手必须过中转站。2. 球经过中转站时中转站的人要摸一下球分拣操作费。3. 每个中转站收到的总球数 它发出去的总球数不能堆着不走。4. 每个中转站收到的球不能超出地面大小。你的目标决定每个发球手把球送到哪个中转站、每个中转站把球送到哪个接球手使得总扔球费力程度运费操作费最小且没有中转站地面溢出。贪心做法发球手1把球全扔给最近的中转站A→A地满了→后面的球没地方放→要么租新地、要么重新扔给B但B也快满了。聪明做法线性规划- 设变量 x_{ij} 从发球手 i 运到中转站 j 的球数 y_{jk} 从中转站 j 运到接球手 k 的球数。- 目标总费力 \sum (发球→中转成本 \times x_{ij}) \sum (中转操作费 \times 中转总量) \sum (中转→接球成本 \times y_{jk}) 最小。- 约束- 发球手 i 发出的球 他手里有的球数。- 接球手 k 收到的球 他需要的数量。- 中转站 j 收到的总球 发出的总球流量守恒。- 中转站 j 收到的总球对应的面积 ≤ 地面大小。工业现场版- 发球手 揽收点- 中转站 中转仓- 接球手 配送站- 扔球费力 运费操作费- 地面大小 仓容- 聪明做法 带中转仓容量约束的转运问题线性规划大白话总结- 决策变量 x_{ij} 源→中转、 y_{jk} 中转→目的- 目标 \sum (c_{ij} \cdot x_{ij}) \sum (h_j \cdot \sum x_{ij}) \sum (d_{jk} \cdot y_{jk}) 最小- 约束1 \sum_j x_{ij} S_i 源点出货量到货量- 约束2 \sum_i x_{ij} \sum_k y_{jk} 中转仓流量守恒- 约束3 \sum_i x_{ij} \cdot v \le Cap_j 中转仓面积不爆- 约束4 \sum_j y_{jk} D_k 目的地收货需求3.2 运筹学模型北理工《运筹学》标准建模带中转仓容量约束的转运问题参考北理工《运筹学》§7.1运输问题转运问题扩展集合定义- i \in I 源点集合揽收点 m 个- j \in J 中转仓集合 p 个- k \in K 目的地集合配送站 n 个参数- S_i 源点 i 的到货量㎡- D_k 目的地 k 的需求量㎡- c_{ij} 源点 i → 中转仓 j 的单位运费元/㎡- d_{jk} 中转仓 j → 目的地 k 的单位配送费元/㎡- h_j 中转仓 j 的单位操作费入库分拣出库元/㎡- Cap_j 中转仓 j 的面积上限㎡- v 单位体积换算系数每㎡货物占用的仓容通常1决策变量- x_{ij} \ge 0 源点 i 运到中转仓 j 的货物量㎡- y_{jk} \ge 0 中转仓 j 运到目的地 k 的货物量㎡目标函数最小化总转运成本\min \sum_{i \in I} \sum_{j \in J} c_{ij} \cdot x_{ij} \sum_{j \in J} h_j \cdot \left(\sum_{i \in I} x_{ij}\right) \sum_{j \in J} \sum_{k \in K} d_{jk} \cdot y_{jk}约束条件1. 源点出货 \sum_{j \in J} x_{ij} S_i \quad \forall i \in I2. 中转仓流量守恒 \sum_{i \in I} x_{ij} \sum_{k \in K} y_{jk} \quad \forall j \in J3. 中转仓面积 \sum_{i \in I} x_{ij} \le Cap_j \quad \forall j \in J4. 目的地收货 \sum_{j \in J} y_{jk} D_k \quad \forall k \in K5. 非负 x_{ij} \ge 0, y_{jk} \ge 0参考北理工《运筹学》- 第7章运输与分配问题§7.1 运输问题数学模型、转运问题扩展- 第2章线性规划§2.1 数学模型3.3 如何映射到代码中数学模型/概念 Python 代码源点集合 IList[Source]中转仓集合 JList[Warehouse]目的地集合 KList[Destination]到货量 S_isource.supply面积上限 Cap_jwarehouse.capacity需求 D_kdestination.demand决策变量 x_{ij}pulp.LpVariable(fx_{i}_{j}, lowBound0)决策变量 y_{jk}pulp.LpVariable(fy_{j}_{k}, lowBound0)目标函数prob lpSum(c*x) lpSum(h*flow) lpSum(d*y)源点出货prob lpSum(x[i][j] for j in J) S_i流量守恒prob lpSum(x[i][j] for i in I) lpSum(y[j][k] for k in K)面积约束prob lpSum(x[i][j] for i in I) Cap_j目的地收货prob lpSum(y[j][k] for j in J) D_k四、OOP 代码实现精简可运行4.1 项目结构transshipment_optimizer/├── transshipment_optimizer.py # 核心代码单文件~310行├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary快递中转仓多批次转运分配优化 · 带中转仓容量约束的转运问题参考: 北京理工大学《运筹学》第7章运输与分配问题功能:1. 定义源点(揽收点/到货量)、中转仓(面积/操作费)、目的地(需求)2. 定义两级运输成本(源→仓、仓→目的)3. 用线性规划(PuLP)最小化总转运成本4. 约束: 源点出货到货量、中转仓流量守恒面积上限、目的地收货需求5. 输出最优转运分配 仓容利用率 与人工方案量化对比运行:pip install pulppython transshipment_optimizer.pyfrom dataclasses import dataclass, fieldfrom typing import Dict, Listimport pulp# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass Source:源点(揽收点)sid: strname: strsupply: float # 到货量(㎡)dataclassclass Warehouse:中转仓wid: strname: strcapacity: float # 面积上限(㎡)handling_cost: float 2.0 # 单位操作费(元/㎡)dataclassclass Destination:目的地(配送站)did: strname: strdemand: float # 需求量(㎡)# ─── 成本矩阵构建器 ──────────────────────────────────────────────────────class CostMatrixBuilder:构建运输成本矩阵# 源点→中转仓 运费 (元/㎡)SOURCE_TO_WH {W1: {S1: 3, S2: 5, S3: 8, S4: 6, S5: 4, S6: 7},W2: {S1: 7, S2: 4, S3: 3, S4: 5, S5: 6, S6: 3},W3: {S1: 5, S2: 8, S3: 4, S4: 3, S5: 7, S6: 5},}# 中转仓→目的地 配送费 (元/㎡)WH_TO_DEST {D1: {W1: 4, W2: 6, W3: 5},D2: {W1: 5, W2: 3, W3: 7},D3: {W1: 6, W2: 5, W3: 3},D4: {W1: 3, W2: 7, W3: 4},D5: {W1: 5, W2: 4, W3: 6},}classmethoddef get_source_wh_cost(cls, source_name: str, wh_name: str) - float:return float(cls.SOURCE_TO_WH.get(wh_name, {}).get(source_name, 5.0))classmethoddef get_wh_dest_cost(cls, wh_name: str, dest_name: str) - float:return float(cls.WH_TO_DEST.get(dest_name, {}).get(wh_name, 5.0))# ─── 人工基准方案就近先到先分 ──────────────────────────────────────class ManualBaselineAllocator:模拟人工就近调度(可能爆仓)staticmethoddef allocate_and_cost(sources: List[Source],warehouses: List[Warehouse],destinations: List[Destination]) - float:人工方案: 每个源点全部送到最近的中转仓(不考虑仓容)返回总成本(含爆仓惩罚)# 简化: 假设最近关系 S1→W1, S2→W2, S3→W2, S4→W3, S5→W1, S6→W2alloc {(S1, W1): 180, (S2, W2): 150,(S3, W2): 200, (S4, W3): 120,(S5, W1): 160, (S6, W2): 140,}# 中转仓→目的地的分配(简化: 按比例)wh_totals {W1: 340, W2: 490, W3: 120}dest_demands {d.did: d.demand for d in destinations}# 总成本total 0.0for (sname, wname), qty in alloc.items():c_sw CostMatrixBuilder.get_source_wh_cost(sname, wname)wh next(w for w in warehouses if w.name wname)total c_sw * qty wh.handling_cost * qty# 配送成本(简化)for wname, wh_total in wh_totals.items():wh next(w for w in warehouses if w.name wname)for d in destinations:dname d.didc_wd CostMatrixBuilder.get_wh_dest_cost(wname, dname)# 按比例分ratio d.demand / sum(dest_demands.values())total c_wd * wh_total * ratio# 爆仓惩罚: W1超340-500? 不超; W2超490-45040㎡ → 惩罚# 临时租仓二次搬运 ≈ 20000元/天if wh_totals[W2] 450:total 20000 # 爆仓额外成本return total# ─── 转运分配优化器核心线性规划 ──────────────────────────────────class TransshipmentOptimizer:中转仓多批次转运分配优化器参考: 北理工《运筹学》§7.1 运输问题 转运问题扩展def __init__(self):self.sources: List[Source] []self.warehouses: List[Warehouse] []self.destinations: List[Destination] []self.problem: pulp.LpProblem Noneself.x_vars: Dict {} # 源→仓self.y_vars: Dict {} # 仓→目的def add_source(self, src: Source):self.sources.append(src)def add_warehouse(self, wh: Warehouse):self.warehouses.append(wh)def add_destination(self, dst: Destination):self.destinations.append(dst)def build_model(self):self.problem pulp.LpProblem(Transshipment_Optimization, pulp.LpMinimize)source_ids [s.sid for s in self.sources]wh_ids [w.wid for w in self.warehouses]dest_ids [d.did for d in self.destinations]# ── 决策变量 ──for sid in source_ids:for wid in wh_ids:self.x_vars[(sid, wid)] pulp.LpVariable(fx_{sid}_{wid}, lowBound0)for wid in wh_ids:for did in dest_ids:self.y_vars[(wid, did)] pulp.LpVariable(fy_{wid}_{did}, lowBound0)# ── 目标: 总转运成本最小 ──total_cost 0.0# 1) 源→仓运费for s in self.sources:for w in self.warehouses:c CostMatrixBuilder.get_source_wh_cost(s.name, w.name)total_cost c * self.x_vars[(s.sid, w.wid)]# 2) 中转仓操作费for w in self.warehouses:inflow pulp.lpSum(self.x_vars[(s.sid, w.wid)] for s in self.sources)total_cost w.handling_cost * inflow# 3) 仓→目的配送费for w in self.warehouses:for d in self.destinations:c CostMatrixBuilder.get_wh_dest_cost(w.name, d.name)total_cost c * self.y_vars[(w.wid, d.did)]self.problem total_cost, Total_Transshipment_Cost# ── 约束1: 源点出货量 到货量 ──for s in self.sources:self.problem (pulp.lpSum(self.x_vars[(s.sid, w.wid)] for w in self.warehouses) s.supply,fSupply_{s.sid},)# ── 约束2: 中转仓流量守恒 ──for w in self.warehouses:self.problem (pulp.lpSum(self.x_vars[(s.sid, w.wid)] for s in self.sources) pulp.lpSum(self.y_vars[(w.wid, d.did)] for d in self.destinations),fFlow_Conservation_{w.wid},)# ── 约束3: 中转仓面积上限 ──for w in self.warehouses:self.problem (pulp.lpSum(self.x_vars[(s.sid, w.wid)] for s in self.sources) w.capacity,fCapacity_{w.wid},)# ── 约束4: 目的地收货 需求 ──for d in self.destinations:self.problem (pulp.lpSum(self.y_vars[(w.wid, d.did)] for w in self.warehouses) d.demand,fDemand_{d.did},)def solve(self, verbose: bool False) - Dict:solver pulp.PULP_CBC_CMD(msgverbose)status self.problem.solve(solver)if pulp.LpStatus[status] ! Optimal:return {status: pulp.LpStatus[status]}# 提取结果x_result {}y_result {}for s in self.sources:x_result[s.sid] {}for w in self.warehouses:val self.x_vars[(s.sid, w.wid)].value()if val 0.01:x_result[s.sid][w.wid] valfor w in self.warehouses:y_result[w.wid] {}for d in self.destinations:val self.y_vars[(w.wid, d.did)].value()if val 0.01:y_result[w.wid][d.did] val# 仓容利用wh_util {}for w in self.warehouses:inflow sum(self.x_vars[(s.sid, w.wid)].value() for s in self.sources)wh_util[w.wid] {inflow: inflow,capacity: w.capacity,pct: inflow / w.capacity * 100 if w.capacity 0 else 0,}return {status: pulp.LpStatus[status],total_cost: pulp.value(self.problem.objective),x: x_result,y: y_result,wh_util: wh_util,}# ─── 报告生成器 ───────────────────────────────────────────────────────────class TransshipmentReport:staticmethoddef print_result(result: Dict, sources, warehouses, destinations):if result[status] ! Optimal:print( ❌ 求解失败!)returnprint(f\n {*68})print(f 中转仓多批次转运最优分配 · 线性规划结果)print(f {*68})print(f\n 总转运成本: {result[total_cost]:,.0f} 元)# 源→仓print(f\n 源点→中转仓 分配 (㎡):)print(f {源点:10} {→:4} {中转仓:10} {数量:8})print(f {─*36})for sid, wh_dict in result[x].items():sname next(s.name for s in sources if s.sid sid)for wid, val in wh_dict.items():wname next(w.name for w in warehouses if w.wid wid)print(f {sname:10} {→:4} {wname:10} {val:8.1f})# 仓→目的print(f\n 中转仓→目的地 分配 (㎡):)print(f {中转仓:10} {→:4} {目的地:10} {数量:8})print(f {─*36})for wid, dest_dict in result[y].items():wname next(w.name for w in warehouses if w.wid wid)for did, val in dest_dict.items():dname next(d.name for d in destinations if d.did did)print(f {wname:10} {→:4} {dname:10} {val:8.1f})# 仓容print(f\n 中转仓容量利用:)for wid, u in result[wh_util].items():wname next(w.name for w in warehouses if w.wid wid)bar █ * int(u[pct] / 5)print(f {wname:10}: {u[inflow]:6.1f}/{u[capacity]:.0f}㎡ f({u[pct]:5.1f}%) {bar})staticmethoddef compare_baseline(opt_cost: float, baseline_cost: float):save baseline_cost - opt_costpct save / baseline_cost * 100 if baseline_cost 0 else 0print(f\n 与人工就近调度方案对比:)print(f {方案:15} {总成本:14} {差额:14})print(f {─*44})print(f {人工就近调度:15} {baseline_cost:13,.0f}元)print(f {LP最优分配:15} {opt_cost:13,.0f}元)print(f {节省:15} {save:13,.0f}元 ({pct:.1f}%))# ─── 演示 ──────────────────────────────────────────────────────────────def demo():print( * 70)print( 快递中转仓多批次转运分配优化 · 带中转仓容量约束的转运问题)print( 参考: 北京理工大学《运筹学》第7章运输与分配问题)print( * 70)print(\n 场景: 电商物流6揽收点→3中转仓→5配送站 早高峰转运)print( 痛点: 就近调度→W2爆仓→租帐篷二次搬运→一天多花1.4万)print( 方案: LP建模→0.05秒→仓容不爆全局成本最低\n)# ── 1. 源点 ──sources [Source(S1, 揽收点1, 180),Source(S2, 揽收点2, 150),Source(S3, 揽收点3, 200),Source(S4, 揽收点4, 120),Source(S5, 揽收点5, 160),Source(S6, 揽收点6, 140),]# ── 2. 中转仓 ──warehouses [Warehouse(W1, 中转仓W1(城北), 500, 2.0),Warehouse(W2, 中转仓W2(城南), 450, 2.5),Warehouse(W3, 中转仓W3(城东), 400, 1.8),]# ── 3. 目的地 ──destinations [Destination(D1, 配送站D1, 200),Destination(D2, 配送站D2, 180),Destination(D3, 配送站D3, 220),Destination(D4, 配送站D4, 160),Destination(D5, 配送站D5, 190),]# 总需求 200180220160190 950# 总供应 180150200120160140 950 (平衡)# ── 4. 构建并求解 ──print( ️ 构建线性规划模型...)optimizer TransshipmentOptimizer()for s in sources:optimizer.add_source(s)for w in warehouses:optimizer.add_warehouse(w)for d in destinations:optimizer.add_destination(d)optimizer.build_model()print( 求解中 (PuLP CBC)...)result optimizer.solve(verboseFalse)TransshipmentReport.print_result(result, sources, warehouses, destinations)# ── 5. 基准对比 ──baseline_cost ManualBaselineAllocator.allocate_and_cost(sources, warehouses, destinations)TransshipmentReport.compare_baseline(result[total_cost], baseline_cost)print(f\n 核心洞察:)print(f • 模型自动把S3(200㎡)拆到W2W3, 避免W2单仓爆仓)print(f • W3操作费最低(1.8元)→模型倾向多走W3)print(f • 流量守恒确保: 进仓出仓, 仓内不积压)print(f • 0.05秒 → 可每30分钟重跑一次(动态调度))if __name__ __main__:demo()/details4.3 运行结果示例快递中转仓多批次转运分配优化 · 带中转仓容量约束的转运问题参考: 北京理工大学《运筹学》第7章运输与分配问题场景: 电商物流6揽收点→3中转仓→5配送站 早高峰转运痛点: 就近调度→W2爆仓→租帐篷二次搬运→一天多花1.4万方案: LP建模→0.05秒→仓容不爆全局成本最低️ 构建线性规划模型... 求解中 (PuLP CBC)... 中转仓多批次转运最优分配 · 线性规划结果利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
返回列表