钢管订购与运输问题:数学建模与Python求解
2026/9/18 0:06:00 网站建设 项目流程

简介:数学建模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_i=1 时累计订购量必须不低于 L_i,y_i=0 时约束退化为非负限制。第三条是需求平衡,每个需求点的到货总量必须精确等于需求量,不允许缺货也不允许多运。

目标函数里 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(c=obj, integrality=integrality, constraints=cons, bounds=bounds) 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, j+n_d, j+2*n_d, ...上。用循环生成约束行而不是手工写每一行,是竞赛建模最容易省时间的地方,尤其是当 n_f 和 n_d 超过两位数时,手写矩阵必然出错。

milpintegrality数组只接受 0 和 1:1 表示该变量必须为整数,0 表示可以连续。实际比赛中,如果钢管数量单位是“根”,整数约束必须保留;如果单位是“吨”且数值较大,可以先按连续变量求解,再把结果代入原目标函数核算误差。

3.2 读懂求解器返回的对象

HiGHS 求解完成后返回一个OptimizeResult对象,我只关注下面几个字段。

返回字段含义使用场景
res.fun最优总成本直接作为论文中的最小费用
res.x所有决策变量的一维数组重排后得到订购量矩阵
res.success是否找到最优解不满足时进入排错流程
res.message求解器给出的状态文本判断是可行解、不可行或边界未收敛

res.x[:n_x]重排成n_fn_d列的矩阵,行代表钢厂,列代表需求点,就能得到一张“钢厂—站点”订购量表:第 i 行第 j 列数值就是 x_ij。竞赛论文里经常出现的结果表,本质就是这张矩阵,再按行求和得到每个钢厂的实际订购量,按列求和验证各需求点是否被满足。

3.3 从 2×3 小例子扩展到赛题规模

常见的 B 题规模是 7 家钢厂加 15 个左右的需求点,此时变量总数是 7×15+7=112 个,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.success=False最常见的提示是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(axis=0), 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 节的三条断言放在求解脚本尾部。队伍里任何成员修改了价格或需求参数,重新运行时都会自动触发检查;只要断言通过,结果表就可以放心交给排版同学。这样处理之后,“全解答”就不只是一个数字,而是一套参数改动后仍能稳定输出的模型系统。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询