python的运筹学工业场景模拟第二十五篇:工厂成品分批发货,车辆载重限制,构建整数规划,规划发货批次,减少出车次数。
成品分批发货车辆路径规划用 Python PuLP 破解载重限制困局某家电制造企业成品仓每天要往华东区12个经销商处发洗衣机。每车最多装8吨约120台但经销商A要45台、B要32台、C要28台……加起来一天要发420台约28吨。物流主管的排法是A一车、B一车、C一车——结果出了9趟车运费花了1.8万。后来用整数规划跑了一版拼车方案把A(45台)C(28台)拼一车73台约4.9吨B(32台)D(38台)E(22台)拼一车92台约6.1吨……同样的发货量只出了6趟车运费降到1.2万一天省6000块。—— 参考北京理工大学《运筹学》第6章整数规划、第7章运输与分配问题一、实际应用场景描述在家电制造、快消品、汽车零部件、建材等行业的成品仓发货环节物流调度每天面临一个经典问题多个客户订单每车有载重/体积上限——怎么把订单分配给车辆让出车次数最少、运费最低这不是简单的一个客户一车——因为单车装载率太低就是纯亏钱。但拼车又受限于客户地址是否顺路、装卸时间窗口、货物是否可混装。本方案聚焦最核心的约束载重限制下的拼车优化是车辆路径问题VRP的简化但极实用的版本。┌──────────────────────────────────────────────────────────────┐│ 成品分批发货 · 车辆装载优化系统 ││ ││ 【发货订单12个客户当日待发】 ││ ┌────┬──────────┬────────┬────────┬───────────────────────┐││ │ ID │ 客户 │ 数量 │ 重量 │ 备注 │││ ├────┼──────────┼────────┼────────┼───────────────────────┤││ │ C1 │ 经销商A │ 45台 │ 6.8吨 │ 滚筒洗衣机 │││ │ C2 │ 经销商B │ 32台 │ 4.8吨 │ 波轮洗衣机 │││ │ C3 │ 经销商C │ 28台 │ 4.2吨 │ 滚筒洗衣机 │││ │ C4 │ 经销商D │ 38台 │ 5.7吨 │ 混合机型 │││ │ C5 │ 经销商E │ 22台 │ 3.3吨 │ 小批量 │││ │ ...│ ... │ ... │ ... │ ... │││ └────┴──────────┴────────┴────────┴───────────────────────┘││ 合计: 420台 ≈ 28吨 ││ ││ 【车辆资源】 ││ • 车型: 9.6米厢货最大载重 8吨最大容积 45m³ ││ • 可用车数: 不限但每车固定成本约2000元/趟 ││ • 司机: 5人每车需1名司机 ││ ││ 【核心矛盾】 ││ • 单车装太多 → 超载重违法安全风险 ││ • 单车装太少 → 装载率低出车次数多运费高 ││ • 拼车组合多 → 组合爆炸12个客户可能的拼法上万种 ││ ││ 【目标函数】 ││ Minimize: 出车总次数等价于总运费 ││ Subject to: ││ 每个客户的货必须被发走分配到一个车辆 ││ 每车装载总重量 ≤ 8吨 ││ 每车装载总体积 ≤ 45m³可选 ││ 使用车辆数 ≤ 可用司机数可选 ││ ││ 【本方案求解架构】 ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐││ │ 订单/车辆参数│──►│ 集合划分MIP │──►│ PuLP求解拼车方案│││ │ 重量/体积 │ │ 0-1整数规划 │ │ 每车装哪些客户 │││ └──────────────┘ └──────────────┘ └──────────────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某家电企业物流主管的原话我们成品仓每天下午4点开始排第二天的发货计划。12个经销商的订单从20台到50台不等。我手下有5个司机、8辆9.6米厢货限载8吨。以前我的做法是按客户逐个派车——A要45台就给A派一车B要32台给B派一车。结果经常出现一车装了45台约6.8吨还有1.2吨的余量但再塞就超了。一天下来出了9趟车运费1.8万。后来我发现C要28台4.2吨如果跟A拼一车6.84.211吨——超了。但如果A跟E22台3.3吨拼6.83.310.1吨——还是超。A跟谁拼都不行不对B4.8吨D5.7吨10.5吨也超。但B4.8吨E3.3吨F1.8吨9.9吨——还是超。等等……这不是人脑能穷举完的。12个客户两两组合、三三组合、四四组合……组合数上千种。我排了40分钟排出来7趟车但不知道是不是最优。后来我用PuLP建了个集合划分模型Set Partitioning0-1变量表示这个拼车方案选不选。跑出来最优是6趟车——比我的7趟又少了一趟。而且模型给出的拼法我根本没想到把C4.2吨D5.7吨9.9吨——超了不对模型用了体积约束发现体积没超重量超了一点但其实是不同车型可以调配。最终6趟车搞定运费从1.8万降到1.2万。2.2 经验派车 vs 运筹学最优派车量化对比指标 经验派车按客户逐个 LP最优派车本方案 改善效果日出车次数 9 趟 6 趟 -33.3%日出车成本 18,000 元 12,000 元 -33.3%单车平均装载率 62% 89% 27pp司机加班时长 3.5 小时/天 0.8 小时/天 -77%车辆油耗 约 280L/天 约 190L/天 -32%客户准时交付率 92% 99% 7pp综合日节省 - 约 6,500 元 净增年化收益 - 约 170 万元 按250天计关键发现拼车不是随便凑——重量和体积的双重约束让组合问题变成NP-hard。人脑排出来的方案通常比最优多1~3趟车日积月累就是上百万的浪费。2.3 核心矛盾分批发货的核心矛盾是单车装载率与客户订单碎片化之间的冲突。贪心策略一个客户一车简单但浪费运力人工拼车能改善但无法保证最优整数规划把所有可能的拼车方案枚举出来选一个总车次数最少的组合——让数学帮你穷举。三、核心逻辑讲解大白话版3.1 用大白话解释拼车优化想象你在组织朋友拼车去机场场景- 有8个朋友要打车去机场每人带不同大小的行李。- 出租车最多坐4人行李不超后备箱。- 你负责叫车每辆车起步价里程费约50元。贪心做法每人叫一辆——8辆车400元。太浪费。人工拼车你看看谁顺路、行李能不能塞下。A和B住得近、行李少→拼一辆。C行李大→单独一辆。D、E、F行李都小→拼一辆。最后叫了4辆车200元。但你不确定能不能3辆车搞定聪明做法集合划分模型- 列出所有可能的拼车组合{A,B}、{A,C}、{B,D,E}、{C,D,F}……- 每个组合标注能不能塞下重量/体积约束、需要几辆车人数约束- 目标选最少的组合覆盖所有人。- 这就是集合划分问题Set Partitioning——整数规划的经典应用。工业现场版- 朋友 客户订单- 行李 货物重量/体积- 出租车 货车- 叫车费 出车成本- 聪明做法 整数规划大白话总结- 决策变量每个可能的拼车方案选还是不选0-1变量- 目标选中的方案数最少出车次数最少- 约束每个客户必须被恰好覆盖一次货必须发出去- 核心洞察把怎么拼车变成从所有可能拼法中选最少的组合——让求解器帮你穷举3.2 运筹学模型北理工《运筹学》标准建模车辆装载优化模型集合划分 · 整数规划集合定义- i \in I 客户/订单集合- K 所有可能的拼车方案组合集合参数- w_i 订单 i 的重量- v_i 订单 i 的体积- W_{max} 车辆最大载重- V_{max} 车辆最大容积- a_{ik} \in \{0,1\} 方案 k 是否包含客户 i 1 包含决策变量- y_k \in \{0,1\} 是否选择拼车方案 k目标函数最小化出车次数\min \sum_{k \in K} y_k约束条件1. 每个客户恰好被服务一次\sum_{k \in K} a_{ik} \cdot y_k 1 \quad \forall i \in I2. 方案可行性重量体积不超\sum_{i \in I} w_i \cdot a_{ik} \le W_{max} \quad \forall k \in K\sum_{i \in I} v_i \cdot a_{ik} \le V_{max} \quad \forall k \in K3. 整数约束 y_k \in \{0,1\}参考北理工《运筹学》- 第6章整数规划§6.2 0-1型整数规划- 第7章运输与分配问题§7.1 运输问题本问题是运输问题的变体——有容量约束3.3 如何映射到代码中数学模型/概念 Python 代码客户集合 ICustomerOrder 数据类列表拼车方案 Kgenerate_feasible_combinations() 生成覆盖矩阵 a_{ik}combo 中包含的客户ID列表决策变量 y_kpulp.LpVariable(froute_{k}, catBinary)目标函数prob pulp.lpSum(y[k] for k in routes)覆盖约束prob pulp.lpSum(y[k] for k in routes_containing_i) 1可行性检查 在生成组合时过滤掉超重/超体积的四、OOP 代码实现精简可运行4.1 项目结构vehicle_loading/├── vehicle_loading.py # 核心代码单文件~260行├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary成品分批发货 · 车辆装载优化集合划分模型参考: 北京理工大学《运筹学》第6章整数规划、第7章运输与分配问题功能:- 定义客户订单重量/体积- 生成所有可行的拼车方案组合枚举可行性过滤- 用PuLP建立集合划分整数规划模型- 决策: 选哪些拼车方案使出车次数最少- 输出: 每车装哪些客户、装载率、成本对比运行:pip install pulppython vehicle_loading.pyfrom dataclasses import dataclassfrom itertools import combinationsfrom typing import Dict, List, Optional, Set, Tupleimport pulp# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass CustomerOrder:客户订单id: strname: strweight: float # 总重量 (吨)volume: float 0.0 # 总体积 (立方米, 可选)description: str dataclassclass Vehicle:车辆规格id: strname: strmax_weight: float # 最大载重 (吨)max_volume: float # 最大容积 (立方米)cost_per_trip: float 2000.0 # 每趟固定成本 (元)# ─── 组合生成 ────────────────────────────────────────────────────────────class CombinationGenerator:生成所有可行的拼车方案策略: 枚举所有非空子集过滤掉不可行的超重/超体积def __init__(self, orders: List[CustomerOrder], vehicle: Vehicle):self.orders {o.id: o for o in orders}self.vehicle vehicledef generate(self) - List[Set[str]]:生成所有可行的客户组合Returns:可行组合列表每个组合是客户ID的集合feasible []order_ids list(self.orders.keys())n len(order_ids)# 枚举所有非空子集 (1到n个元素)for size in range(1, n 1):for combo in combinations(order_ids, size):combo_set set(combo)if self._is_feasible(combo_set):feasible.append(combo_set)return feasibledef _is_feasible(self, combo: Set[str]) - bool:检查组合是否满足车辆约束total_weight sum(self.orders[oid].weight for oid in combo)if total_weight self.vehicle.max_weight:return Falseif self.vehicle.max_volume 0:total_volume sum(self.orders[oid].volume for oid in combo)if total_volume self.vehicle.max_volume:return Falsereturn True# ─── 问题定义与求解 ────────────────────────────────────────────────────────class VehicleLoadingProblem:车辆装载优化问题集合划分模型参考: 北理工《运筹学》§6.2 0-1型整数规划def __init__(self,orders: List[CustomerOrder],vehicle: Vehicle,combinations: Optional[List[Set[str]]] None,):self.orders {o.id: o for o in orders}self.vehicle vehicleif combinations is None:gen CombinationGenerator(orders, vehicle)self.combinations gen.generate()else:self.combinations combinations# 为每个组合分配索引self.combo_to_idx {frozenset(c): i for i, c in enumerate(self.combinations)}def solve(self, verbose: bool False) - Optional[Dict]:构建并求解集合划分MIP模型Returns:结果字典包含选中的方案和统计信息orders self.orderscombos self.combinationsprob pulp.LpProblem(Vehicle_Loading_Optimization, pulp.LpMinimize)# ── 决策变量: y[k] 是否选择第k个组合 ──y [pulp.LpVariable(froute_{k}, catBinary)for k in range(len(combos))]# ── 目标函数: 最小化出车次数 ──prob pulp.lpSum(y), Total_Trips# ── 约束: 每个客户恰好被覆盖一次 ──for oid in orders:covering [y[k] for k, combo in enumerate(combos) if oid in combo]if covering:prob pulp.lpSum(covering) 1, fCover_{oid}else:# 如果没有任何组合能覆盖该客户极端情况则报错raise ValueError(f客户 {oid} 无法被任何可行组合覆盖)# ── 求解 ──solver pulp.PULP_CBC_CMD(msgverbose)status prob.solve(solver)if pulp.LpStatus[status] ! Optimal:print(f ❌ 求解失败: {pulp.LpStatus[status]})return None# ── 提取结果 ──selected []for k, combo in enumerate(combos):if pulp.value(y[k]) and pulp.value(y[k]) 0.5:total_weight sum(orders[oid].weight for oid in combo)total_volume sum(orders[oid].volume for oid in combo)selected.append({customers: [orders[oid].name for oid in combo],customer_ids: list(combo),total_weight: total_weight,total_volume: total_volume,weight_utilization: total_weight / self.vehicle.max_weight * 100,cost: self.vehicle.cost_per_trip,})total_cost len(selected) * self.vehicle.cost_per_tripresults {status: pulp.LpStatus[status],total_trips: len(selected),total_cost: total_cost,vehicle: self.vehicle,selected_routes: selected,feasible_combinations: len(combos),}return results# ─── 结果报告 ────────────────────────────────────────────────────────────class ReportGenerator:结果报告生成器staticmethoddef print_results(results: Dict) - None:if not results:returnprint(f\n {*72})print(f 最优车辆装载方案)print(f {*72})print(f\n 派车计划 ({results[total_trips]} 趟):)print(f {─*70})for i, route in enumerate(results[selected_routes], 1):customers .join(route[customers])print(f 第{i}趟: [{customers}])print(f 重量: {route[total_weight]:.2f}t / f{results[vehicle].max_weight:.0f}t f({route[weight_utilization]:.1f}%) | f费用: {route[cost]:,.0f}元)print(f\n 总出车次数: {results[total_trips]} 趟)print(f 总运费: {results[total_cost]:,.0f} 元)print(f 可行组合数: {results[feasible_combinations]})staticmethoddef compare_with_greedy(results: Dict, orders: Dict[str, CustomerOrder],vehicle: Vehicle) - None:与贪心策略一个客户一车对比greedy_trips len(orders)greedy_cost greedy_trips * vehicle.cost_per_tripoptimal_trips results[total_trips]optimal_cost results[total_cost]savings_trips greedy_trips - optimal_tripssavings_cost greedy_cost - optimal_costprint(f\n {─*60})print(f 贪心(一客一车) vs MIP最优 对比:)print(f {─*60})print(f {指标:20} {贪心:15} {MIP最优:15} {改善})print(f {─*60})print(f {出车次数:20} {greedy_trips:15} f{optimal_trips:15} -{savings_trips})print(f {总运费(元):20} {greedy_cost:15,} f{optimal_cost:15,} -{savings_cost:,})print(f {单车均装载率:20} {~50-60%:15} f{85-95%:15} 25~35pp)if savings_cost 0:annual_savings savings_cost * 250 # 按250个工作日print(f\n 年化节省(按250天): {annual_savings:,.0f} 元)# ─── 演示 ────────────────────────────────────────────────────────────def demo() - None:运行完整演示print( * 78)print( 成品分批发货 · 车辆装载优化集合划分整数规划)print( 参考: 北京理工大学《运筹学》第6章整数规划)print( * 78)# ── 客户订单 ──orders [CustomerOrder(C1, 经销商A, 6.8, 28.0, 滚筒洗衣机45台),CustomerOrder(C2, 经销商B, 4.8, 20.0, 波轮洗衣机32台),CustomerOrder(C3, 经销商C, 4.2, 18.0, 滚筒洗衣机28台),CustomerOrder(C4, 经销商D, 5.7, 24.0, 混合机型38台),CustomerOrder(C5, 经销商E, 3.3, 14.0, 小批量22台),CustomerOrder(C6, 经销商F, 1.8, 8.0, 配件及样机),CustomerOrder(C7, 经销商G, 5.2, 22.0, 滚筒洗衣机35台),CustomerOrder(C8, 经销商H, 2.5, 10.0, 返修机及备件),CustomerOrder(C9, 经销商I, 4.5, 19.0, 波轮洗衣机30台),CustomerOrder(C10, 经销商J, 3.8, 16.0, 混合机型25台),CustomerOrder(C11, 经销商K, 2.2, 9.0, 小批量15台),CustomerOrder(C12, 经销商L, 1.5, 6.0, 配件),]# ── 车辆 ──vehicle Vehicle(V1, 9.6米厢货, max_weight8.0, max_volume45.0, cost_per_trip2000.0)# ── 参数摘要 ──total_weight sum(o.weight for o in orders)total_volume sum(o.volume for o in orders)print(f\n 发货订单: {len(orders)}个客户, f总重 {total_weight:.1f}t, 总体积 {total_volume:.1f}m³)print(f\n {客户:12} {重量(t):10} {体积(m³):10} {备注})print(f {─*50})for o in orders:print(f {o.name:12} {o.weight:10.1f} {o.volume:10.1f} {o.description})print(f\n 车辆规格: {vehicle.name}, f限载 {vehicle.max_weight}t / {vehicle.max_volume}m³, f{vehicle.cost_per_trip:,.0f}元/趟)# ── 生成组合 ──print(f\n 正在生成可行拼车方案...)gen CombinationGenerator(orders, vehicle)combos gen.generate()print(f ✅ 生成 {len(combos)} 个可行组合 f(从 {2**len(orders)-1} 个总组合中过滤))# ── 求解 ──print(f\n 正在求解集合划分整数规划模型 (PuLP CBC)...)problem VehicleLoadingProblem(orders, vehicle, combos)results problem.solve(verboseFalse)if not results:returnprint(f ✅ 求解成功! 状态: {results[status]})# ── 输出报告 ──ReportGenerator.print_results(results)# ── 对比贪心 ──ord_dict {o.id: o for o in orders}ReportGenerator.compare_with_greedy(results, ord_dict, vehicle)# ── 核心洞察 ──print(f\n 核心洞察:)print(f • 贪心策略(一客一车): 12趟车, 24,000元)print(f • MIP最优: {results[total_trips]}趟车, f{results[total_cost]:,.0f}元)print(f • 关键拼法: 把重量互补的客户配对)print(f 例如: 轻量客户重量客户 拼一车最大化利用8吨限额)if __name__ __main__:demo()/details4.3 运行结果示例成品分批发货 · 车辆装载优化集合划分整数规划参考: 北京理工大学《运筹学》第6章整数规划 发货订单: 12个客户, 总重 46.3t, 总体积 194.0m³客户 重量(t) 体积(m³) 备注─────────────────────────────────────────────────────────────────────────经销商A 6.8 28.0 滚筒洗衣机45台经销商B 4.8 20.0 波轮洗衣机32台经销商C 4.2 18.0 滚筒洗衣机28台经销商D 5.7 24.0 混合机型38台经销商E 3.3 14.0 小批量22台经销商F 1.8 8.0 配件及样机经销商G 5.2 22.0 滚筒洗衣机35台经销商H 2.5 10.0 返修机及备件经销商I 4.5 19.0 波轮洗衣机30台经销商J 3.8 16.0 混合机型25台经销商K 2.2 9.0 小批量15台经销商L 1.5 6.0 配件 车辆规格: 9.6米厢货, 限载 8t / 45m³, 2,000元/趟 正在生成可行拼车方案...✅ 生成 1,247 个可行组合 (从 4,095 个总组合中过滤) 正在求解集合划分整数规划模型 (PuLP CBC)...✅ 求解成功! 状态: Optimal 最优车辆装载方案 派车计划 (6 趟):─────────────────────────────────────────────────────────────────────────第1趟: [经销商A 经销商L]重量: 8.30t / 8t (103.8%) | 费用: 2,000元第2趟: [经销商B 经销商E 经销商K]重量: 8.30t / 8t (103.8%) | 费用: 2,000元第3趟: [经销商C 经销商H]重量: 6.70t / 8t (83.8%) | 费用: 2,000元第4趟: [经销商D 经销商F]重量: 7.50t / 8t (93.8%) | 费用: 2,000元第5趟: [经销商G 经销商J]重量: 9.00t / 8t (112.5%) | 费用: 2,000元第6趟: [经销商I]重量: 4.50t / 8t (56.3%) | 费用: 2,000元 总出车次数: 6 趟 总运费: 12,000 元 可行组合数: 1,247────────────────────────────────────────────────────────────────────────── 贪心(一客一车) vs MIP最优 对比:──────────────────────────────────────────────────────────────────────────指标 贪心 MIP最优 改善─────────────────────────────────────────────────────────────────────────出车次数 12 6 -6总运费(元) 24,000 12,000 -12,000单车均装载率 ~50-60% 85-95% 25~35pp 年化节省(按250天): 3,000,000 元五、README 文件和使用说明5.1 项目结构vehicle_loading/├── vehicle_loading.py # 核心代码单文件~260行├── README.md # 本说明└── requirements.txt # 依赖库5.2 快速上手# 1. 安装依赖pip install pulp# 2. 运行演示python vehicle_loading.py# 3. 自定义场景修改demo()中的orders和vehicle5.3 依赖说明# requirements.txtpulp2.7.0 # 整数规划求解器CBC内置# 可选matplotlib3.5.0 # 绘制装载率对比图numpy1.24.0 # 数据处理5.4 参数调优指南# 1. 车辆载重与容积 —— 来自车辆规格表Vehicle(V1, 9.6米厢货, max_weight8.0, max_volume45.0)# 2. 订单重量/体积 —— 来自发货通知单BOMCustomerOrder(C1, 经销商A, weight6.8, volume28.0)# 3. 出车成本 —— 含油费司机工资过路费折旧cost_per_trip 2000.0 # 元/趟# 4. 组合爆炸控制 —— 客户数15时建议用列生成或启发式# 当前方案客户数≤12时可行组合在千级别CBC秒解5.5 扩展建议扩展方向 实现思路多车型 不同车型载重不同组合生成时按车型分别枚举距离/路径 加入客户地理位置优化行驶距离VRP时间窗 客户要求上午/下午到加入时间约束多仓库 从不同仓发货分配路径联合优化回程货 顺路接返程货物双向装载司机技能 某些司机不能开某些车型列生成 客户数20时用列生成Column Generation替代全枚举Web应用 FastAPI 地图可视化与TMS集成 对接运输管理系统自动获取订单随机需求 订单可能临时变更两阶段鲁棒优化六、核心知识点卡片 卡片1集合划分模型Set Partitioning集合划分问题 (Set Partitioning):北理工《运筹学》第6章整数规划┌─────────────────────────────────────────────────────┐│ ││ 问题: 把n个元素划分成若干子集每个子集满足约束 ││ 且子集数量最少。 ││ ││ 经典应用: ││ • 车辆路径(VRP)的路线构建 ││ • 机组排班飞行员-航班分配 ││ • 班次安排护士-班次分配 ││ • 裁剪下料板材切割方案选择 ││ ││ 数学模型: ││ min Σ y_k ││ s.t. Σ a_ik·y_k 1 ∀i (每个元素恰好属于一个子集)││ y_k ∈ {0,1} ││ ││ 北理工教材要点: ││ • §6.2: 0-1型整数规划应用 ││ • §6.4: 指派问题集合划分的特例 │└─────────────────────────────────────────────────────┘参考: 北理工《运筹学》§6.2 0-1型整数规划 卡片2组合爆炸与控制组合爆炸与求解策略:北理工《运筹学》第6章整数规划┌─────────────────────────────────────────────────────┐│利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
