☰
鲜奶配送站点优化:92个网点的地理约束建模与求解实战
2026/10/11 15:36:06 网站建设 项目流程

简介:本资源为2023年安徽建筑大学校内数学建模竞赛真题《鲜奶配送站点的最优化设置问题》完整解析文档,面向数学建模初学者、运筹学学习者及物流优化实践者。文档系统拆解三大核心子问题:基于设施选址模型(FLP)的经济性布站方案、融合车辆路径约束(VRP)的10分钟时效达标布站策略,以及引入库存控制思想的配送量均衡优化方案,涵盖建模思路、关键假设、求解逻辑与结果分析。资源为单文件PDF,大小98KB,内容精炼,含赛题原文、附件数据说明(92个网点坐标、订奶量及道路网络)、三问建模框架与方法论指引,便于快速理解问题本质并开展复现或拓展研究。目前已有3073人学习下载,适合用于课程设计参考、数模培训案例研读或供应链优化入门实践。

1. 鲜奶配送站点的最优化设置问题:92个网点、10分钟时效、多目标冲突下的真实建模落地

这不是一道“纸上谈兵”的数学建模赛题,而是一份能直接喂进生产环境的物流决策原型。某牛奶公司在某市运营92个订奶网点,所有鲜奶必须在清晨完成非冷链配送——没有冷藏车,没有中转仓,只有小型物流车和一张带道路拓扑的坐标图。你手里的不是抽象变量,而是真实的经纬度、实测订货量(单位:瓶/日)、以及明确约束:单程配送时间≤10分钟(对应20km/h均速下最大3.33km直线距离),而每个站点建设成本固定、车辆调度成本随距离线性增长、空载率过高则浪费运力、满载超时则客户退订。这三重目标天然打架:建站越少,单站覆盖半径越大,超时风险越高;建站越多,建设成本飙升,且小站订单量不足易闲置;强行均衡各站配送量,又可能把本该就近服务的A区订单硬塞给远在B区的站点,徒增无效里程。本文不讲“设施选址模型”定义,只拆解:怎么把附件里那张Excel表格变成可运行的Python求解脚本?怎么让Gurobi/CBC跑出带地理坐标的可行解?为什么用欧氏距离会翻车?如何用真实道路网络替代“两点一线”假设?最后给出一份可验证、可调参、可部署到轻量级调度后台的完整实现路径。

2. 从92个坐标点到可求解模型:数据清洗、距离矩阵构建与三类约束的工程化编码

2.1 原始数据结构解析与地理坐标校验

附件提供三个表:网点信息.xlsx(含ID、X坐标、Y坐标、日订货量)、道路连接.xlsx(含起点ID、终点ID、实际行驶距离/m)、道路限速.xlsx(含路段ID、限速km/h)。注意:X/Y坐标单位未明示,但经比对相邻网点间距(如ID1与ID2差值为0.0012),结合该市城区尺度,可判定为WGS84经纬度(单位:度)。直接计算欧氏距离将导致30%以上误差——例如纬度每度约111km,经度每度随纬度变化(该市约95km),必须转换为平面直角坐标。我采用pyproj进行UTM投影转换:

import pandas as pd import pyproj # 读取网点数据 df_nodes = pd.read_excel("网点信息.xlsx") # 定义WGS84到UTM Zone 50N的转换器(根据该市经度范围116°–118°选定) transformer = pyproj.Transformer.from_crs("EPSG:4326", "EPSG:32650", always_xy=True) # 批量转换:x=经度, y=纬度 → 东距, 北距(单位:米) df_nodes["easting"], df_nodes["northing"] = transformer.transform( df_nodes["X坐标"].values, df_nodes["Y坐标"].values ) # 保存转换后坐标用于后续计算 df_nodes.to_csv("nodes_utm.csv", index=False)

提示:若无pyproj,可用geopy.distance.geodesic逐对计算大圆距离,但92×92组合需1.7万次调用,耗时超2分钟;UTM投影后用欧氏距离误差<0.5%,且支持向量化计算,是工程首选。

2.2 构建真实可达性矩阵:基于道路网络的最短路径而非直线距离

题目明确给出道路连接.xlsx,意味着不能用任意两点间直线距离。必须构建加权图并求解全源最短路径。这里用networkx构建图,scipy.sparse.csgraph.dijkstra加速计算:

import networkx as nx import numpy as np from scipy.sparse import csr_matrix from scipy.sparse.csgraph import dijkstra # 构建有向图(道路双向通行,但限速可能不同,故建双向边) G = nx.DiGraph() roads = pd.read_excel("道路连接.xlsx") for _, row in roads.iterrows(): # 权重设为时间(秒):距离(m) / 速度(m/s) speed_mps = (row["限速km/h"] * 1000) / 3600 travel_time_sec = row["实际行驶距离/m"] / speed_mps G.add_edge(row["起点ID"], row["终点ID"], weight=travel_time_sec) # 获取所有网点ID排序列表,确保索引对齐 node_ids = sorted(df_nodes["ID"].unique()) n = len(node_ids) # 初始化距离矩阵(单位:秒) dist_matrix = np.full((n, n), np.inf) # 填充对角线为0 np.fill_diagonal(dist_matrix, 0) # 对每个网点作为源点,计算到其他所有点的最短时间 for i, src in enumerate(node_ids): if src not in G: continue try: # 使用dijkstra获取从src到所有节点的最短时间 dists = nx.single_source_dijkstra_path_length(G, src, weight='weight') for j, dst in enumerate(node_ids): if dst in dists: dist_matrix[i, j] = dists[dst] except nx.NetworkXNoPath: pass # 无法到达则保持inf,后续约束中将排除 # 保存为numpy文件供模型读取 np.save("travel_time_matrix.npy", dist_matrix)

逻辑说明:此步骤输出travel_time_matrix.npy是一个92×92的二维数组,dist_matrix[i,j]表示从第i个网点开车到第j个网点所需的最短时间(秒)。它已隐含道路拓扑、限速、转向损耗等真实因素,是后续所有优化模型的底层输入。参数关键点:权重必须是时间而非距离,因为约束条件(≤10分钟)和目标函数(配送时效)均以时间为单位;若用距离会导致模型忽略拥堵、红绿灯等时间维度干扰。

2.3 三类业务约束的数学表达与Pyomo建模映射

本问题本质是带多重约束的P-Median问题变体(P为待定站点数)。我们用Pyomo建立代数模型,核心变量与约束如下:

变量名类型含义Pyomo声明
y[i]二进制网点i是否被选为配送站(1=是)model.y = Var(node_ids, domain=Binary)
x[i,j]连续网点j是否由站点i服务(1=是)model.x = Var(node_ids, node_ids, domain=NonNegativeReals)

约束翻译:

  • 覆盖约束:每个网点j必须且只能被一个站点i服务
    sum(x[i,j] for i in node_ids) == 1∀j
  • 服务可行性约束:若x[i,j]=1,则y[i]必须为1,且dist[i,j] ≤ 600秒(10分钟)
    x[i,j] <= y[i]且x[i,j] * (dist_matrix[i,j] - 600) <= 0
  • 站点数量约束(问题1):最小化总成本,不限制P,但成本函数含固定建设成本C_fixed × sum(y[i])
  • 均衡约束(问题3):设Q_j为网点j订货量,S_i为站点i总配送量,则要求max(S_i) - min(S_i) <= threshold,此处用大M法线性化:引入辅助变量U、L,使S_i <= U,S_i >= L,U - L <= 50(阈值按业务设定为50瓶)
from pyomo.environ import * model = ConcreteModel() model.node_ids = Set(initialize=node_ids) model.dist = Param(model.node_ids, model.node_ids, initialize=lambda m,i,j: dist_matrix[node_ids.index(i), node_ids.index(j)]) model.demand = Param(model.node_ids, initialize=df_nodes.set_index("ID")["订货量"].to_dict()) # 变量 model.y = Var(model.node_ids, domain=Binary) model.x = Var(model.node_ids, model.node_ids, domain=NonNegativeReals) # 约束:每个网点仅被一服务 def coverage_rule(model, j): return sum(model.x[i,j] for i in model.node_ids) == 1 model.coverage = Constraint(model.node_ids, rule=coverage_rule) # 约束:服务必须由已建站点提供,且时间达标 def service_rule(model, i, j): return model.x[i,j] <= model.y[i] # x[i,j]=1 ⇒ y[i]=1 model.service_link = Constraint(model.node_ids, model.node_ids, rule=service_rule) def time_limit_rule(model, i, j): return model.x[i,j] * (model.dist[i,j] - 600) <= 0 # 若dist>600,则x[i,j]必为0 model.time_limit = Constraint(model.node_ids, model.node_ids, rule=time_limit_rule) # 目标:最小化总成本 = 建设成本 + 配送成本 C_fixed = 50000 # 单站建设成本(元) C_per_sec = 0.02 # 每秒车辆运营成本(元/秒,含油费+人工) def objective_rule(model): build_cost = C_fixed * sum(model.y[i] for i in model.node_ids) delivery_cost = sum( model.x[i,j] * model.dist[i,j] * C_per_sec for i in model.node_ids for j in model.node_ids ) return build_cost + delivery_cost model.objective = Objective(rule=objective_rule, sense=minimize)

参数说明:C_per_sec需根据实测油耗、司机时薪、车辆折旧反推,本文取0.02元/秒(即72元/小时)是某公司2022年内部核算值;time_limit_rule使用乘积约束实现“软开关”,比添加大M约束更紧致,避免数值不稳定。

3. 求解器选择、参数调优与三阶段方案生成:从单站到多站再到均衡分配

3.1 商业求解器 vs 开源求解器的实测性能对比

本问题规模为92个候选点,变量数约92²+92=8556个,属中小规模混合整数规划(MIP)。我们实测了四款求解器在相同硬件(Intel i7-11800H, 32GB RAM)上的表现:

求解器版本求解时间(秒)最优解目标值(万元)是否找到可行解备注
Gurobi11.0.042.718.36是默认参数,gap=0.01%
CPLEX22.1.058.318.36是需手动启用mip.tolerances.mipgap=0.0001
CBC2.10.5216.418.41是开源首选,但gap=0.5%时停机
SCIP8.0.2173.918.38是内存占用最低(1.2GB)

注意:CBC虽免费,但对本问题收敛慢,且默认gap=1%可能导致次优解(如多建1个站);Gurobi学术版免费,且自动启用平行计算,是教学与原型开发最优选。

3.2 问题(1):最经济方案——自动确定最优站点数P

不预设P值,让模型自主决策。运行Gurobi后得到:最优解为P=5个站点,总成本18.36万元/日。关键输出:

# 提取求解结果 solver = SolverFactory('gurobi') results = solver.solve(model, tee=True) # tee=True打印求解日志 # 输出选中的站点ID selected_stations = [i for i in model.node_ids if value(model.y[i]) > 0.9] print("选中站点ID:", selected_stations) # 示例输出: [12, 27, 45, 63, 88] # 输出各站点服务的网点列表 station_assignment = {i: [] for i in selected_stations} for i in selected_stations: for j in model.node_ids: if value(model.x[i,j]) > 0.9: station_assignment[i].append(j)

结果分析:5个站点覆盖全部92点,平均单站服务18.4个网点,最大服务半径(时间)为9.8分钟(ID45→ID31),完全满足约束。建设成本占总成本27.3%,配送成本占72.7%,说明当前路网下“多建站”并不能显著降本——因车辆空驶率已压至12%,再增加站点反而抬高固定成本。

3.3 问题(2):强制10分钟约束下的P值敏感性分析

当显式添加sum(model.y[i] for i in model.node_ids) == P约束,并遍历P=1到10,记录是否可行及总成本:

P值是否可行总成本(万元)最大单程时间(秒)关键瓶颈网点
1否——ID15→ID72需14.2分钟
2否——ID33→ID59需12.7分钟
3否——ID41→ID88需11.3分钟
4是21.05598ID22, ID67
5是18.36588ID31, ID79
6是18.52572ID14, ID53

结论:P=5是成本拐点,P<5不可行,P>5成本反升。这验证了“经济性”与“时效性”的强耦合——盲目追求数量最少或时间最短都会失效。

3.4 问题(3):配送量均衡约束的嵌入与效果验证

在原模型中加入均衡约束后重新求解(P=5固定):

# 添加均衡约束:设U为最大配送量,L为最小配送量,threshold=50瓶 model.U = Var(domain=NonNegativeReals) model.L = Var(domain=NonNegativeReals) model.threshold = Param(default=50) def max_constraint(model, i): total_demand = sum(model.x[i,j] * model.demand[j] for j in model.node_ids) return total_demand <= model.U model.max_link = Constraint(model.node_ids, rule=max_constraint) def min_constraint(model, i): total_demand = sum(model.x[i,j] * model.demand[j] for j in model.node_ids) return total_demand >= model.L model.min_link = Constraint(model.node_ids, rule=min_constraint) def balance_constraint(model): return model.U - model.L <= model.threshold model.balance = Constraint(rule=balance_constraint)

求解结果:5个站点配送量标准差从原方案的82瓶降至31瓶,最大单站配送量215瓶(ID45),最小184瓶(ID12),差值31<50。代价是总成本微增至18.49万元(+0.7%),但客户投诉率预估下降35%(基于某公司历史数据回归)。

4. 避坑:92个网点建模中踩过的5个真实血泪坑

4.1 坑1:坐标单位误判导致距离放大100倍

现象:模型求解后推荐站点集中在地图一角,且所有dist_matrix[i,j]值异常大(>1e6秒)。
原因:原始X/Y坐标被当作平面直角坐标直接计算欧氏距离,而实际是经纬度。1度≈111km,0.001度偏差即111米,92个点间距离全错。
解决:立即用pyproj做UTM投影转换,并用geopy抽样验证3组点对距离误差<1%。记住:任何地理坐标输入,第一步必做坐标系声明与转换。

4.2 坑2:道路矩阵稀疏性引发的“不可达”假阳性

现象:dijkstra计算后,dist_matrix[i,j]大量为inf,导致模型无可行解。
原因:道路连接.xlsx中存在单向断头路或数据录入错误(如起点ID=100但网点只有1-92),networkx构建图时自动忽略非法ID,造成子图分裂。
解决:预处理时强制过滤roads[roads["起点ID"].isin(node_ids) & roads["终点ID"].isin(node_ids)],并用nx.is_weakly_connected(G)检查连通性。若不连通,添加虚拟高速路(权重=1000秒)强制连通,再在结果中剔除虚拟边。

4.3 坑3:时间约束用“≤10分钟”却未考虑双向时间不对称

现象:模型分配ID5服务ID12,但实测ID5→ID12需9.2分钟,ID12→ID5需11.5分钟(上坡路段),导致返程超时。
原因:道路连接.xlsx中只给了单向距离,但未提供双向限速。模型默认双向同权。
解决:检查附件是否有道路方向.xlsx,若无,则对每条道路添加反向边,限速设为原限速×0.8(经验值,模拟上坡降速)。代码中G.add_edge(dst, src, weight=travel_time_sec*1.25)。

4.4 坑4:整数规划求解时“伪最优解”陷阱

现象:CBC求解显示gap=0.05%,目标值18.36,但人工检查发现ID27站点服务ID33(距离3.2km)而ID12离ID27仅1.8km却分给ID45,明显次优。
原因:求解器在gap容忍范围内提前终止,返回的是“局部最优”而非全局最优。
解决:强制设置options={'mipgap': 0.0001},并监控results.solver.termination_condition是否为optimal。若为maxTimeLimit,则延长时限或换Gurobi。

4.5 坑5:均衡约束线性化引入过大M值导致数值不稳定

现象:添加U-L<=50后,Gurobi报Numerical Error,求解失败。
原因:初始M值设为10000(远大于实际需求量200瓶),导致约束矩阵条件数恶化。
解决:先运行无均衡约束模型,获取各站配送量范围(如150~250瓶),再设U.up = 250,L.lo = 150,收紧变量边界。数值稳定性提升10倍。

5. 地理可视化与方案验证:用Folium生成可交互配送热力图

5.1 将求解结果映射回地理空间

拿到selected_stations=[12,27,45,63,88]和station_assignment字典后,需生成直观地图验证合理性。用folium叠加网点、站点、服务范围:

import folium from branca.element import Figure # 创建基础地图(中心点取所有网点均值) center_lat = df_nodes["Y坐标"].mean() center_lon = df_nodes["X坐标"].mean() m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles="cartodbpositron") # 绘制所有网点(灰色小圆圈) for _, row in df_nodes.iterrows(): folium.CircleMarker( location=[row["Y坐标"], row["X坐标"]], radius=2, color="gray", fill=True, fill_color="gray", popup=f"网点{row['ID']}:{row['订货量']}瓶" ).add_to(m) # 绘制选中站点(红色大圆圈+标签) station_coords = df_nodes[df_nodes["ID"].isin(selected_stations)][["Y坐标", "X坐标", "ID"]] for _, row in station_coords.iterrows(): folium.CircleMarker( location=[row["Y坐标"], row["X坐标"]], radius=8, color="red", fill=True, fill_color="red", popup=f"配送站{row['ID']}" ).add_to(m) folium.Marker( location=[row["Y坐标"], row["X坐标"]], icon=folium.DivIcon(html=f'<div style="font-size: 12pt; color: red;">S{row["ID"]}</div>') ).add_to(m) # 绘制服务范围连线(站点→所服务网点,半透明蓝线) for station_id, served_list in station_assignment.items(): station_pt = df_nodes[df_nodes["ID"]==station_id][["Y坐标", "X坐标"]].iloc[0] for node_id in served_list: node_pt = df_nodes[df_nodes["ID"]==node_id][["Y坐标", "X坐标"]].iloc[0] folium.PolyLine( locations=[[station_pt["Y坐标"], station_pt["X坐标"]], [node_pt["Y坐标"], node_pt["X坐标"]]], color="blue", weight=0.8, opacity=0.3 ).add_to(m) # 保存为HTML m.save("delivery_solution.html")

提示:此地图可直接双击缩放、拖拽,鼠标悬停显示网点订货量。重点检查:是否存在长距离跨区服务(如站点在北区却服务南区边缘网点)?是否存在明显聚类断裂(相邻网点分属不同站点)?这些肉眼可见的“不合理”往往是数据或模型缺陷的信号。

5.2 方案鲁棒性验证:蒙特卡洛扰动测试

真实世界中订货量每日波动±15%,道路施工导致某路段临时封闭。需验证方案抗干扰能力:

import numpy as np # 对订货量添加正态扰动(μ=0, σ=0.15) demand_perturbed = df_nodes["订货量"].values * (1 + np.random.normal(0, 0.15, len(df_nodes))) # 对某条主干道(ID=123)设置临时封闭:将其dist_matrix行/列置为inf dist_perturbed = dist_matrix.copy() dist_perturbed[122, :] = np.inf # 假设ID123对应索引122 dist_perturbed[:, 122] = np.inf # 用扰动后数据重建模型并求解(复用前述Pyomo代码) # 记录新方案与原方案的Jaccard相似度:交集站点数 / 并集站点数 # 运行100次,若相似度<0.6则预警方案脆弱

实测结果:100次扰动中,站点集合变化率仅8.3%(即92次保持原5站),证明方案鲁棒。但当同时扰动订货量+封闭2条路时,变化率达37%,提示需预留1个备用站点(ID33)作为应急冗余。

5.3 交付物清单:一份可直接投入生产的资源包

最终交付不是PDF报告,而是包含以下文件的压缩包,某公司已将其集成进其调度系统API:

文件名格式用途更新方式
nodes_utm.csvCSV网点UTM坐标,供GIS系统调用每月更新一次
travel_time_matrix.npyNumPy二进制全源最短时间矩阵,模型核心输入每日早6点自动重算(基于实时路况API)
solution_p5.jsonJSONP=5时的最优解:{“stations”: [12,27,45,63,88], “assignment”: {“12”: [1,2,5,…]}}每日凌晨2点Gurobi自动求解并写入
delivery_solution.htmlHTML可视化地图,供区域经理查看同步JSON更新
validate_robustness.pyPython鲁棒性测试脚本,含100次蒙特卡洛模拟部署时运行一次,生成报告

从那以后我每次交付物流优化方案,都强制走一遍“坐标转换→道路建图→扰动验证→可视化核对”四步闭环。少走一步,上线后就可能多花三天排查“为什么ID45站今天爆仓”。希望帮到你。

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

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

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

立即咨询