钢管订购与运输问题:数学建模与Python求解

发布时间:2026/9/18 0:06:05
钢管订购与运输问题:数学建模与Python求解 简介数学建模B题钢管订购和运输全解答是一份面向数学建模竞赛参赛者的完整优化方案聚焦钢管订购与运输调度问题。包含一个Word文档约459KB系统梳理了从问题分析、模型建立到求解的全过程采用Floyd算法求解铁路与公路网最短路径构造供需运输价格表并以总费用最小为目标建立非线性规划模型使用Lingo求出问题一最优解1278632万元、问题二最优解1407149万元还讨论了树形管道网络的推广情形。文档同时包含灵敏度分析指出S5钢厂销价和S1钢厂产量上限对购运计划及总费用影响最大并给出相应数字结果。已有557人学习下载适合需要快速掌握此类运输优化建模方法与Lingo应用技巧的学生参考。1. 从“最便宜钢厂”直觉反例说起的钢管订购和运输问题把整条管线的订购量全部压给出厂单价最低的钢厂是新手最先想到、也最容易翻车的方案价格低的往往离需求点远运输网络里的转运瓶颈会把账面省下来的差价成倍吃掉。数学建模B题钢管订购和运输本质是同时决定三个问题向哪几家钢厂订购多少根钢管、沿哪条路线送到哪个站点、管道沿线各分段的需求如何被满足最终目标是让采购成本加运输成本的总和最小。这类题在数学建模国赛、华为杯和研究生数学建模竞赛里反复出现标准解法是“最短路压缩路网 混合整数线性规划”。下面按照我实际做题的顺序展开先建费用矩阵再写数学模型然后用 Python 求解接着做敏感性分析和排错最后给出能直接写进论文的结果验证方法。2. 建模三步走把钢管订购和运输变成数学规划问题2.1 先分类三份原始数据价格、运输网络、管道需求这类题的题干排版年年变但拆开之后永远是三块数据。第一块是钢厂参数每个厂给出出厂单价通常还带产量上限改进版题目会额外增加最低起订量或者单次发货的固定费用。第二块是交通网络铁路、公路、水运的费率和里程各不相同题目一般给一张带节点和里程的拓扑图节点之间有直连边也可能经过中转站。第三块是管道需求沿线的关键站点确定之后相邻站点之间的每一段管道都会对应一个需求长度整条管线的总需求要和钢厂的供应能力对得上。我用下表的划分方式来整理原始数据避免建模到一半才发现某个字段没用到。数据模块原始字段在模型中的作用钢厂信息出厂单价、产量上限、最低起订量、固定订货费目标函数的采购单价以及产能约束的右端项运输网络铁路/公路/水运里程、分段费率、节点连接关系预计算为“钢厂i到需求点j”的单位运输费用矩阵管道铺设段站点编号、相邻站点间需求长度、单位铺设成本形成需求平衡约束铺设成本通常作为常数项处理需要特别留意“最低起订量”。只要题目出现这个字段模型就不能退化成纯线性规划因为“完全不用某个钢厂”对应 0 根而“启用该钢厂”至少要订 L 根这是一条跨过零点的跳变约束必须引入 0/1 变量。倘若题目没有最低起订量也没有固定订货费那么 0/1 变量可以省掉直接用连续变量求解速度会快很多。2.2 用 Floyd 最短路算法把路网压成费用矩阵在构造目标函数之前我会先把交通网络简化成一张二维费用矩阵第 i 行第 j 列表示“从钢厂 i 运 1 根钢管到需求点 j”的最小运输费用。这一步我固定使用 Floyd-Warshall 算法因为竞赛题的路网节点数通常只有几十个O(n³) 的复杂度完全可接受手写三循环比调试堆优化的 Dijkstra 更不容易出错真遇到上千节点的路网再换成scipy.sparse.csgraph里的现成函数。下面是一个 6 节点的小例子节点 0、1 是钢厂节点 2~5 是需求点边的权值已经是“运送 1 根钢管的花费”import numpy as np n 6 # 节点编号0/1 是钢厂2~5 是需求点 inf float(inf) w np.full((n, n), inf) for i in range(n): w[i, i] 0.0 # 以下每条边权值统一为“运送1根钢管的花费”不是纯粹距离 w[0, 2] 120; w[2, 0] 120 # S1 - A1 w[1, 3] 95; w[3, 1] 95 # S2 - A2 w[2, 3] 37; w[3, 2] 37 # A1 - A2 w[3, 4] 51; w[4, 3] 51 # A2 - A3 w[4, 5] 42; w[5, 4] 42 # A3 - A4 w[0, 1] 80; w[1, 0] 80 # S1 - S2 d w.copy() for k in range(n): # k 是中转点 for i in range(n): # i 是起点 for j in range(n): # j 是终点 if d[i, k] d[k, j] d[i, j]: d[i, j] d[i, k] d[k, j] print(d[[0, 1], 2:]) # 输出2家钢厂到4个需求点的最小运输成本三层循环的含义是依次允许节点 0~5 作为中转点每次比较“绕道 k”是否比当前路径更便宜。循环结束后第 i 行第 j 列就是全局最小费用。这里最容易翻车的地方是直接把里程当权值。如果铁路按里程区间分段计价或者公路有重载折扣就先要把里程套进题目的费率公式换算成“元/根”之后才能填入 w。最短路算法寻找的是费用最小路径不是物理距离最短路径两者在分段计价下经常不一致。2.3 决策变量与混合整数规划的完整写法经过 2.2 节之后路网已经从模型里消失剩下四组输入数据出厂单价 p_i、单位运输费用 c_ij、钢厂产能 U_i、各需求点需求量 d_j。如果题目带最低起订量再记 L_i如果带固定订货费记 f_i。决策变量定义如下。符号含义类型x_ij钢厂 i 运往需求点 j 的钢管根数非负整数y_i钢厂 i 是否被启用0/1 变量目标函数与约束条件写成$$\min \quad \sum_{i\in I}\sum_{j\in J}(p_i c_{ij})x_{ij} \sum_{i\in I}f_i y_i$$$$\text{s.t.} \quad \sum_{j\in J}x_{ij} \le U_i,y_i \quad \forall i$$$$\sum_{j\in J}x_{ij} \ge L_i,y_i \quad \forall i$$$$\sum_{i\in I}x_{ij} d_j \quad \forall j$$$$x_{ij}\ge 0,\ x_{ij}\in \mathbb{Z}, \quad y_i\in{0,1}$$第一条约束限制产能x_ij 的累计订购量不能超过钢厂产能同时通过 U_i·y_i 保证“不启用该厂时订购量强制为 0”。第二条处理最低起订量y_i1 时累计订购量必须不低于 L_iy_i0 时约束退化为非负限制。第三条是需求平衡每个需求点的到货总量必须精确等于需求量不允许缺货也不允许多运。目标函数里 p_i 和 c_ij 能直接相加是因为两者单位一致都是“元/根”。单位铺设成本没有写进目标函数因为总需求固定时它是常数不影响最优决策若题目要求报告总成本计算完最优解后把它单独加回来即可。3. 用 Python 把钢管订购和运输的最小费用模型跑通3.1 两钢厂三需求点的最小可用求解脚本我把第三节的模型直接翻译成 Python。求解器选用 SciPy 自带的milp函数它基于 HiGHS 求解器对百级变量规模的竞赛题完全够用不依赖 Gurobi 或 CPLEX 的许可证。import numpy as np from scipy.optimize import milp, LinearConstraint, Bounds price np.array([420, 410]) # 出厂价元/根 freight np.array([[30., 25., 22.], [28., 30., 45.]]) # 单位运输成本元/根 cap np.array([800., 900.]) # 产能上限根 demand np.array([200., 300., 400.]) # 需求点需求量根 low np.array([100., 150.]) # 最低起订量根 n_f, n_d freight.shape n_x n_f * n_d # 运输变量个数 # 决策变量顺序[x11,x12,x13,x21,x22,x23, y1,y2] obj np.concatenate([(price[:, None] freight).ravel(), np.zeros(n_f)]) # 固定订货费先按0处理 # 需求约束每个需求点的到货量总和必须等于d_j eqA np.zeros((n_d, n_x n_f)) for j in range(n_d): eqA[j, np.arange(j, n_x, n_d)] 1.0 eqb demand # 产能约束与起订量约束 ubA np.zeros((2 * n_f, n_x n_f)) for i in range(n_f): ubA[i, i * n_d:(i 1) * n_d] 1.0 # 该厂总订购量 ubA[i, n_x i] -cap[i] # sum - cap*y 0 ubA[n_f i, i * n_d:(i 1) * n_d] -1.0 # -sum low*y 0 ubA[n_f i, n_x i] low[i] ubb np.zeros(2 * n_f) cons [LinearConstraint(eqA, eqb, eqb), LinearConstraint(ubA, -np.inf, ubb)] # y 变量限制在 [0,1]加上整数性后自动等价于 0/1 ub np.full(n_x n_f, np.inf) ub[n_x:] 1.0 bounds Bounds(np.zeros(n_x n_f), ub) # x 和 y 都按整数变量处理 integrality np.ones(n_x n_f) res milp(cobj, integralityintegrality, constraintscons, boundsbounds) print(res.message) print(总成本:, res.fun) print(订购量矩阵:\n, res.x[:n_x].reshape(n_f, n_d))这段代码里eqA[j, np.arange(j, n_x, n_d)] 1.0是固定的排列技巧因为变量按“i 外循环、j 内循环”排列第 j 个需求点的相关变量正好落在索引j, jn_d, j2*n_d, ...上。用循环生成约束行而不是手工写每一行是竞赛建模最容易省时间的地方尤其是当 n_f 和 n_d 超过两位数时手写矩阵必然出错。milp的integrality数组只接受 0 和 11 表示该变量必须为整数0 表示可以连续。实际比赛中如果钢管数量单位是“根”整数约束必须保留如果单位是“吨”且数值较大可以先按连续变量求解再把结果代入原目标函数核算误差。3.2 读懂求解器返回的对象HiGHS 求解完成后返回一个OptimizeResult对象我只关注下面几个字段。返回字段含义使用场景res.fun最优总成本直接作为论文中的最小费用res.x所有决策变量的一维数组重排后得到订购量矩阵res.success是否找到最优解不满足时进入排错流程res.message求解器给出的状态文本判断是可行解、不可行或边界未收敛把res.x[:n_x]重排成n_f行n_d列的矩阵行代表钢厂列代表需求点就能得到一张“钢厂—站点”订购量表第 i 行第 j 列数值就是 x_ij。竞赛论文里经常出现的结果表本质就是这张矩阵再按行求和得到每个钢厂的实际订购量按列求和验证各需求点是否被满足。3.3 从 2×3 小例子扩展到赛题规模常见的 B 题规模是 7 家钢厂加 15 个左右的需求点此时变量总数是 7×157112 个milp求解几乎瞬时完成。需要改的只是数据读取部分freight np.loadtxt(freight_matrix.csv, delimiter,) assert freight.shape (n_f, n_d), 运费矩阵尺寸与钢厂数、需求点数不一致我的建议是无论题目给的是 Excel 还是 PDF第一步都整理成两个 CSV一个是钢厂参数表一个是需求点表运费矩阵则在 2.2 节计算后直接保存。把数据读取和模型求解拆成两个脚本文件后面做敏感性分析时只需修改输入文件不需要改动模型代码。如果求解规模继续扩大比如需求点细分到上百个管段milp仍然能解但迭代时间会明显增加。此时优先把整数约束放宽成连续变量试一次若连续解与整数解的目标函数差距很小说明整数性不是敏感因素可以在论文里说明并采用连续模型。4. 参数边界、敏感性扫描与无解问题排查4.1 四个最容易把结果带偏的参数钢管订购和运输题的正确率往往不取决于模型有多复杂而取决于参数预处理是否严谨。我总结出四个高频出错点。参数常见错误建议处理方式铁路运费用一个平均单价代替分段计价先把里程套进费率公式换成“元/根”再跑最短路计量单位吨、公里、根混用全部统一成“元/根”“公里”“根”最低起订量忽略或写成 x 的下界用 low·y 约束而不是直接令 x≥low需求拆分把一段管道需求当成单点需求按相邻节点拆成多个需求点再输入模型“最低起订量”这一项尤其容易写错。有人把它写进 x 的非负下界结果是每个钢厂都必须下单哪怕只订 1 根也会触发起订约束正确的做法是把起订量与启用变量 y 绑定。若题目没有起订量把low设成 0 即可模型不变。4.2 对需求量和价格做敏感性扫描评委在答辩中最常问的问题是“如果需求量增长 10%成本会怎么变”。为了让回答有数据支撑我通常把第三节的建模过程封装成一个函数def run_model(demand, price, freight, cap, low): # 把 3.1 中的约束生成和 milp 调用放进这里 # 返回求解结果 res return res for beta in [0.90, 0.95, 1.00, 1.05, 1.10]: res run_model(demand * beta, price, freight, cap, low) print(f需求 {beta:.0%}: 总成本 {res.fun:.1f})参数说明beta 表示需求量缩放系数0.90 表示需求下降 10%1.10 表示需求上升 10%每次调用都会重建需求约束并重新求解。把结果存成列表后再生成一张“增长率—总成本—各钢厂订购量”的三列表格这就是论文里的敏感性分析。需要强调的是敏感性分析不是把代码拷五遍而是把求解过程函数化。函数入参只有需求量、价格、运费、产能和起订量五组数据任何参数变化都能在同一套框架里跑。4.3 求解器报 infeasible 时的三条排查路线res.successFalse最常见的提示是PRIMAL_INFEASIBLE意思是模型约束之间存在矛盾。按下面顺序排查通常几分钟内能找到问题第一检查总需求是否超过总产能。把 demand 求和与 cap 求和对比如果需求大于产能模型天然无解这时候需要检查题目是否允许部分需求由其他供应方式补充。第二检查运费矩阵是否有inf行或inf列。某家钢厂到某个需求点之间没有任何通路时最短路结果仍是无穷大约束会强制该变量为 0如果某一个需求点对所有钢厂都不可达需求约束就不可能满足。用一条断言即可定位assert np.isfinite(freight).all(), 运费矩阵存在不可达路径第三检查最低起订量与产能约束是否冲突。比如某钢厂产能只有 80 根但起订量是 100 根U_i L_i 会让 y_i 无论取 0 还是 1 都导致约束矛盾。出现这种情况时要么把该厂从模型里剔除要么在论文中单独说明该厂无法参与供货。5. 钢管订购和运输模型的结果验证与论文呈现5.1 最优性自检的三道关口得到一个漂亮的res.fun之后不要急着写进论文。历年数学建模优秀论文里最终结果表后面通常都跟着一段验证说明这比结果本身更能体现建模严谨性。我的自检流程固定在求解脚本末尾# 第1步求解状态必须为成功 assert res.success, res.message # 第2步用原始变量手动重算总成本 mat_x res.x[:n_x].reshape(n_f, n_d) manual_cost (price[:, None] freight) * mat_x manual_cost manual_cost.sum() assert abs(res.fun - manual_cost) 1e-6, 目标函数重算不一致 # 第3步每个需求点是否恰好满足 assert np.allclose(mat_x.sum(axis0), demand), 需求约束未满足三道检查分别对应最优性、目标函数一致性和需求可行性。第 2 步看起来是重复计算但它能拦截一类很隐蔽的错误比如变量顺序写反导致打印出来的结果矩阵和实际目标函数不对应。如果想进一步说明“整数约束不影响最优总成本”可以再跑一次连续版本把integrality全部设为 0对比连续最优值与整数最优值。两者差值越小论文里就越有底气说明“整数约束不是成本敏感因素”。5.2 用结构化提示词把求解结果整理成论文表格近两年的数学建模竞赛中用大模型辅助已经成为常态搜索“claude数学建模好用吗”能看到大量经验帖。我的判断是通用 AI 适合做表格整理和文字润色但必须给它结构化输入否则它会一本正经地编造采购量。我从求解脚本里导出变量数组之后会把变量顺序和数值原样贴给 AI提示词固定写成下面这个模板我有 scipy.optimize.milp 的输出 变量顺序[x11,x12,x13,x21,x22,x23,y1,y2] 变量数值[0, 180, 220, 200, 120, 180, 1, 1] 请整理成“钢厂×需求点”的 Markdown 表格 并分别计算每个钢厂的总订购量和每个需求点的到货量。 只输出结果表格不要修改任何数值。变量顺序必须和代码中的排列完全一致AI 整理的表格才能直接进论文。如果你使用的是专门的数学建模智能体也可以让它直接读 CSV 文件但最终数值仍然要以脚本里res.x为准。这个工作流的关键一步是把 5.1 节的三条断言放在求解脚本尾部。队伍里任何成员修改了价格或需求参数重新运行时都会自动触发检查只要断言通过结果表就可以放心交给排版同学。这样处理之后“全解答”就不只是一个数字而是一套参数改动后仍能稳定输出的模型系统。本文还有配套的精品资源点击获取