ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

库存路径问题与混合整数规划:原材料订购转运联合优化实战

库存路径问题与混合整数规划:原材料订购转运联合优化实战 简介一份面向数学建模竞赛的PDF文档聚焦生产企业原材料的订购与转运方案优化适合正在准备数学建模比赛或研究供应链决策问题的学生参考。文档完整呈现四个问题的建模与求解思路先用预处理和冒泡排序筛选重要供应商再结合总产能约束制定24周订购与转运方案并针对A、B、C三类原材料比例偏好及产能扩大情景进行优化最后给出转运商损耗率与方案实施效果分析。资源为单个PDF文件大小约1MB包含摘要、问题重述、问题分析、模型构建与结果讨论可直接用于论文框架借鉴和算法复盘。已有3147人学习适合需要系统梳理供应商选择、订货量匹配、转运损耗控制与产能提升计算方法的读者。1. 生产企业的原材料订购与转运本质是一道“库存-路径”联合优化题把“原材料的订购和转运方案”这个选题拿到手里很多参赛队第一反应是拆成两个独立步骤先用评分法选供应商再对着地图把转运路线排一遍。两件事分开做工作量确实小但会丢掉二者之间最关键的那层联系——订购量决定了每周需要转运的货量转运车辆的容量反过来又限制了单周能订多少材料。任何一个环节单独最优合并起来往往让仓库要么爆仓、要么断料。数学建模在这道题里的位置不是把公式写得漂亮而是把“向谁订、订多少、怎么运、运多少”写成一组能够同时成立的约束再交给求解器去收敛。这类问题在供应链里被称为库存-路径问题Inventory Routing Problem, IRP也是国赛 C 题里经常出现的生产计划题材。下文按建模、代码、数据处理、求解器调参到论文定稿的顺序还原一版能落地、能答辩、能被评委挑不出大毛病的完整方案。2. 把订购和转运写进同一套混合整数规划2.1 从供应商到产线原材料订购与转运的流程链任何生产企业的原材料流动都可以抽象成一条闭合链原材料仓库发出订购需求供应商按订购单备货转运车辆把货从供应商运到厂区仓生产消耗库存库存低于安全水位后再触发下一轮订购。转运方案不只是“车怎么开”它还决定了订购周期的粒度。常见做法是按周或按旬把整个计划期切成周期每个周期末统计库存下一个周期初让车辆到达这样模型才不会在连续时间上无限复杂。如果把供应商维度加进来流程链上会出现两个天然约束。第一每个供应商只能供应特定品类不是所有材料都能从同一家买到。第二供应商有自己的最大供货能力这个能力随周期波动不能只看平均值。两者叠加后“向谁订”就变成一个带品类覆盖和容量限制的 0-1 选择问题“运多少”则要受车辆容量约束单周期运进厂的总量不能超过可用车辆的载重上限。订购决策 → 供应商品类覆盖 → 周期供货上限 → 到货入库存 → 生产消耗 → 下一周期订购2.2 决策变量、目标函数与约束把转运方案用公式卡死模型里最容易写漏的是“供应商启用”和“转运量”之间的逻辑关系。我一般会引入三组决策变量。第一组是 0-1 变量z_i表示是否启用第i个供应商第二组是整数变量x_ijt表示第t周期向供应商i订购原材料j的数量第三组是库存变量I_jt表示第t周期末材料j的库存量。目标函数建议写成五项之和供应商启用的固定订购成本、材料单价成本、转运运输成本、库存持有成本和单位缺货惩罚。其中缺货惩罚特别关键若把它设为无穷大模型会变成硬性不允许断料在很多供应紧张的场景下直接无解设为有限值求解器才有余地输出“在某某周期可以接受少量缺货”的松弛方案。核心约束需要四组同时成立。供应商供货能力约束每个周期从该供应商订购的所有材料总重量不超过其最大供货能力启用逻辑约束z_i 0时所有x_ijt必须为 0库存平衡约束I_jt I_j(t-1) 到货量 - 需求量转运容量约束单周期内所有供应商运入的货物总重量不超过车辆总载重。这些约束在数学上不复杂难的是它们之间的指数级组合关系这才是 MIP 求解器真正要处理的部分。2.3 为什么先建 MIP 而不是先跑遗传算法很多参赛队一上来就写遗传算法或粒子群因为“显得高级”但这类启发式在这个标题下的最大问题是不可验证。GA 跑出目标函数值后你无法判断这个值离真正的最优解有多远也没有一个礼貌的指标告诉评委“这个解有多好”。混合整数规划天然输出 MIP Gap也就是当前最优上界和下界之间的相对差这个值可以直接写进论文作为方案质量的客观证据。我一般先建 MIP小规模样例用 CBC 求解跑不顺再切 Gurobi如果模型规模大到 MIP 无法在合理时间内收敛再用启发式生成初解、用 MIP 精修这才是数学建模里“混合”二字的正确用法。3. Python 实现订购-转运联合求解的最小代码3.1 用 PuLP 写一个可运行的 MIP 雏形先做一个四周期以内的精简版本验证模型逻辑正确再扩大数据量。下面的代码只保留了两家供应商、两种材料和两个周期但目标函数和约束骨架和正式赛题完全同构。import pulp suppliers [S1, S2] materials [M1, M2] periods [0, 1] capacity {S1: 30, S2: 28} # 单周期最大供货量 order_cost {S1: 120, S2: 100} # 启用供应商的固定订购成本 price {(S1,M1): 8, (S1,M2): 5, (S2,M1): 6, (S2,M2): 7} # 材料单价 demand {(0,M1): 10, (0,M2): 8, (1,M1): 9, (1,M2): 10} # 各周期材料需求 freight {S1: 2.0, S2: 3.5} # 单位材料转运成本 model pulp.LpProblem(order_transport, pulp.LpMinimize) x pulp.LpVariable.dicts(x, (suppliers, materials, periods), 0, None, pulp.LpInteger) u pulp.LpVariable.dicts(u, suppliers, catBinary) model pulp.lpSum(order_cost[s] * u[s] for s in suppliers) \ pulp.lpSum((price[(s, m)] freight[s]) * x[s][m][t] for s in suppliers for m in materials for t in periods) for s in suppliers: for t in periods: model pulp.lpSum(x[s][m][t] for m in materials) capacity[s] * u[s] for m in materials: for t in periods: model pulp.lpSum(x[s][m][t] for s in suppliers) demand[(t, m)] model.writeLP(order_transport.lp) solver pulp.PULP_CBC_CMD(msgTrue, timeLimit30, gapRel0.02) model.solve(solver) print(pulp.LpStatus[model.status])这段代码的逻辑说明目标函数第一行是供应商固定订购成本第二行把材料单价和转运成本合并这样订购量同时承担采购和运输费用供应商约束里的capacity[s] * u[s]是一把双刃剑——既允许单个供应商供货又保证未启用的供应商不会出货需求约束用大于等于而不是等于给库存留了余量。writeLP把模型 dump 成 LP 文件方便后续切 Gurobi 或对照检查约束稀疏程度。gapRel0.02表示当上下界差距在 2% 以内时就提前停止这是竞赛中平衡时间和质量的标准做法。3.2 启发式与 MIP 混合先给一个可行解再去精修当供应商数量超过 50 家、周期超过 12 个时CBC 的收敛速度会明显下滑。我一般先用一个简单启发式生成初始解再把它作为 warm start 喂给求解器。Gurobi 支持通过MIPStart文件指定初始解PuLP 中也可以在求解前遍历变量对一部分变量赋予初值CBC 会把可行解作为树搜索的第一个上界。这个方法对求解时间的改善不是线性的经常能把第一可行解从几分钟压缩到几秒钟后半段全靠 branch-and-cut 逐步收紧下界。初始解的生成方式不需要复杂按材料单价升序排序优先从便宜供应商订到容量上限剩余缺量按第二便宜供应商补足最后用车辆容量倒推周期订购上限。这个贪心解可能浪费了转运成本但作为 MIP 的起点已经够用。贪心解质量越差MIP 需要剪掉的枝越多所以我会把单位里程转运成本也放进排序键而不只按单价排序。3.3 三个必调的求解器参数Gap、时间限制和 MIPFocus求解器参数不需要全会三个能解决问题就够了。第一个是 MIPGap决定提前终止的精度阈值第二个是 TimeLimit防止模型在交卷前夜跑一整晚不收敛第三个是 MIPFocus当模型长期找不到可行解时把求解器的搜索重心从证明下界切换到寻找可行解。参数默认值推荐值使用场景MIPGap1e-40.01 ~ 0.02供应商大于 50 家时追求 1% 以内即可TimeLimit无限120 ~ 300 秒论文提交前强制限时附带日志截图作证据MIPFocus02前 30 秒找不到首个可行解时启用VarBranch-13变量规模大优先使用最小不可行度分支Threads0全部4 ~ 8在竞赛机器上避免多任务抢占 CPU关于 MIPFocus 的一个常见误用模型已经能在 10 秒内找到可行解但下界松此时把 MIPFocus 设为 2 反而拖慢收敛。正确策略是先跑 30 秒看日志如果根松弛下界和目标值之间的 gap 一直下不来再调 MIPFocus3 去强化界这才是对症下药。4. 数据清洗与规模裁剪输入不干净模型再漂亮也是白搭4.1 供应商供货记录的清洗与交付稳定性画像赛题给的供应商历史数据通常有很多坑收货量比订购量大、同一供应商有重复记录、少数周期完全没供货。直接把这类数据灌进模型产能约束会失真。我一般分三步处理。第一步按“供应商-材料-周期”做主键去重同一周期重复记录取最大值而不是平均值——因为最大值代表该供应商在全力运转时的真实能力。第二步把收货量超过订购量 5% 的记录标记为超供超供部分不直接丢弃而是折算成这个供应商的“可超量系数”在模型里给它的 capacity 乘一个 1.05 的上浮空间。第三步是计算供应商画像指标用一段 pandas 聚合输出平均供货量、变异系数和最大单期供货量import pandas as pd df[over_flag] df[收货量] df[订购量] * 1.05 stats df.groupby(供应商).agg( 平均供货量(收货量, mean), 变异系数(收货量, lambda x: x.std() / x.mean() if x.mean() 0 else 9), 最大单期(收货量, max) ) stats[稳定型] stats[变异系数] 0.3这段代码的核心是变异系数变异系数小于 0.3 的供应商可以认为交付稳定在模型里可以把它们的 capacity 按均值设定变异系数大于 0.5 的供应商要按最大单期供货量的 80% 作为 capacity否则模型会在它超水平发挥的周期上过度压货实际运行必然落空。稳定型标记在论文里可以做成一个分布饼图评委对这类“从数据到模型参数”的推导过程非常买账。4.2 转运成本矩阵与车辆容量约束的正确建法转运成本不能只按供应商到厂区的距离折算还要把车辆容量加进去。常见做法是把每个供应商到仓库的运输成本拆成“整车成本”和“零担成本”两段整车模式下每车运费是固定的超过一车部分按比例递增零担模式下按重量或体积计价。对数学建模题而言整车和零担的切换会让模型多出整数变量求解变慢。我一般采取一个折中按每单位材料的平均运费折算但在约束里保留车辆总载重限制这样成本是线性的容量是硬约束求解器负担最小。车辆容量约束还有一个容易忽略的点如果供应商分布在不同的方向单周期把所有出库量加总做容量约束是合理的但如果题目还要求车辆从不同供应商处集货后再回厂就要额外加入路径变量。原题如果没有明确说“一车只能去一个供应商”就不要主动引入 VRP 路径变量那会显著增加求解难度而且没有收益。4.3 周期粒度选择从 24 期压缩到 8 期的真实代价原题材料需求往往是按天或按周给的动辄几十个周期。直接按天建模整数变量会膨胀到不可解。正确的做法是先按自然周聚合再看总周数是否超过 12 期超过就继续按每 4 周合并成一个订购周期。合并时不能简单把需求相加还要检查合并后的最小库存是否出现过负值merged_demand raw_demand.resample(4W).sum() min_level raw_demand.resample(4W).apply( lambda x: (x.cumsum() - x.sum()).min() ) if min_level.min() -安全库存: print(折叠后存在缺货窗口请按 2W 重试)这段代码说明一个关键坑把周期变粗后如果原始粒度下某个子周期内需求集中爆发合并求和会把这段紧缺抹平。所以折叠后要用“最严子周期库存”去校核而不是只看总需求。周期粒度选定后所有供应商 capacity、车辆容量和库存上限都要换算到同一周期口径这是模型参数一致性的底线。5. 可解性诊断与灵敏度分析卡住时的四步排查5.1 无可行解时先查这五个约束MIP 在求解初期报 infeasible绝大多数时候不是数据问题而是约束逻辑冲突。排查顺序我固定在五条。第一供应商启用约束与需求约束是否冲突即所有启用供应商的容量之和小于总需求第二库存平衡约束是否把初始库存漏掉了导致第一周期需求无法被满足第三车辆容量是否小于单个供应商在某个周期的最大出货量如果是容量约束直接砍掉了最便宜的供应来源第四整数变量上下界是否过窄比如材料订购量设置了非零下界但该供应商根本没有供货能力第五缺货惩罚设为无穷大后模型不允许缺货但需求峰值超过所有渠道供应能力之和。Root relaxation: objective 3.8213e05 MIP start: objective 5.2040e05 Nodes | Current Node | Objective Bounds | Work ... | 3.8213e05 | Gap 26.6%日志里最关键的不是目标值而是 Root relaxation 和 MIP start 之间的相对距离。上面示例里根松弛目标值是 38 万MIP start 是 52 万gap 高达 26.6%说明当前启发式初解质量很差需要先改进 warm start而不是调整 MIPFocus。如果日志在同一 gap 上停了超过 60 秒我不建议死等而是回到供应商选择集合去观察是不是某个高价但稳定的供应商被成本目标排斥在解外导致可行域被切断。5.2 带料率与单位缺货惩罚的灵敏度曲线灵敏度分析是这个题里最容易被评委打分的内容。把单位缺货惩罚系数从 1 逐步调到 100记录每个取值下最优解的总成本、缺货总次数和启用供应商数量。这三个值的变化会形成一条典型曲线惩罚系数很低时模型选择让周转运成本降低宁可缺货也不多订惩罚系数到达某个阈值后缺货次数骤降但总成本斜率变陡。这个拐点的倒数本质上就是缺货成本在优化模型里的影子价格。实操时可以用一个循环批量跑模型每次只改动 penalty 系数把目标值、缺货量和供应商数量写入表格。赛后写论文时这个表非常有用评委问“为什么最终方案能保证供货”直接引用惩罚系数大于临界值后的稳定区间即可不需要模糊解释。5.3 求解器参数再调整按 Gurobi 日志判断下一步如果换用 Gurobi日志的阅读方式和 CBC 基本相同但多了Incumbent和BestBd两列。当 Incumbent 长时间不变而 BestBd 持续增长曲线走得是收敛当两个数字都纹丝不动问题多半出在模型结构上而不是参数。此时再调 Threads 或 MIPGap 已经没有意义正确操作是回到线性和整数约束检查是否存在对称性——两个供应商的品类覆盖和价格完全一致会让 branch-and-bound 重复探索大量对称解。解决办法很简单给其中一个供应商增加一条小小的容量差异化约束比如 S1 的 capacity 比 S2 少 5%对称性打破后求解速度经常呈数量级提升。6. 给“终 1”版本的最后一轮打磨Benders 分解与符号说明6.1 把大规模实例拆成 主问题 子问题 的 Benders 分解思路当供应商超过 60 家、周期超过 12 个时完整 MIP 模型即使给 300 秒也很难收敛到 2% 以内。这个规模下我一般会把模型按决策类型拆成两层。主问题只保留供应商选择变量z_i和周期订购变量x_ijt先求解一个“忽略转运容量”的松弛问题得到一个目标值下界把主问题的解固定下来之后转运子问题就变成一个纯粹的可线性网络流问题检查车辆容量是否被违反。如果违反就生成一条 Benders feasibility cut附加回主问题要求下一轮在这组订购量下给出可执行的转运方案。迭代 3 到 5 轮主问题的目标值和子问题的实际成本会逐步收敛到同一个区间。实现时不要一开始就写回调函数。先在 Python 循环里手写 10 轮 Benders 迭代观察上界单调下降、下界单调上升的大趋势。趋势对后再改写成 Gurobi 的 LazyConstraint callback这样调试成本低很多也能在论文里画出收敛曲线作为算法有效性的直接证据。6.2 论文里的符号说明要与代码变量一一对应终版命名多了“终 1”说明模型和算法已经迭代到收尾阶段。此时最容易被忽视的问题是论文符号表和代码变量名脱节。我见过不少方案公式里用Q_ijt表示订购量附录代码里却叫x[s][m][t]答辩时评委一眼就能看出论文和程序不是同一套体系。建议在最终版本里统一成下表样式符号含义单位z_i第 i 个供应商是否启用0-1x_ijt第 t 周期向供应商 i 订购材料 j 的数量吨I_jt第 t 周期末材料 j 的库存量吨cap_it供应商 i 在周期 t 的最大供货能力吨U_t周期 t 可用的转运车辆总载重吨p_pen单位缺货成本元/吨做完符号统一后再补一个随机性验证把历史数据随机剔除 5% 的记录重跑一遍记录目标值与原始解之间的相对偏差。这个偏差小于 3%就可以在论文里写“方案对数据扰动不敏感”如果偏差超过 10%说明方案过度依赖个别供应商的高峰供货记录最终版里必须把这家供应商的 capacity 下调或者把它从备选集合中移除。这个操作比再调一版求解器参数更能决定论文能不能进优秀论文候选。答辩时评委问的第一个问题往往是“模型参数从哪来”能从数据清洗到灵敏度分析讲清这条链路这个题才真正算收口。本文还有配套的精品资源点击获取
返回列表