ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

鲜奶配送站选址建模:从最短路径到集合覆盖的数学优化方案

鲜奶配送站选址建模:从最短路径到集合覆盖的数学优化方案

简介:面向数学建模竞赛与运筹学学习者的鲜奶配送站点最优化PDF文档,聚焦92个配送网点下的设施选址与路径规划问题。文件基于2023年安徽建筑大学校内数模赛题,内容涵盖三个核心子问题:经济性选址、10分钟时间窗约束下的车辆路径,以及配送量均衡控制,并给出Facility Location、Vehicle Routing、Inventory Control等模型思路。包体为1个PDF,文件大小仅98KB,轻量精炼,便于快速查阅。目前已有3073人学习下载,适合备赛冲刺或供应链优化专题实践。通过该题可系统演练数学建模中的多目标优化与约束处理,掌握从问题分解到模型选择的完整方法论。

1. 引言:鲜奶“最后一公里”的配送站选址,为什么值得当一道建模题

做数学建模的同学第一眼看到这道题,通常会被 92 个网点、订奶量和配送道路表镇住,以为要解一个巨大的车辆路径问题。其实拆开看,它问的只有一件事:在这 92 个候选位置里,到底建几个鲜奶配送站、建在哪,才能让时效、成本和配送量三者都说得过去。鲜奶配送不同于普通快递,车辆非冷链、只能覆盖城市局部区域,配送时间一旦超过保鲜窗口,整批货就报废。所以这个问题不是单纯求最短路径,而是设施选址与区域划分的组合优化。适合正在做方案或毕设的你:物流配送网络优化章节完全可以照这条思路搭。看懂这一篇,后面怎么处理附件、怎么建模、怎么求解、怎么答辩就全清楚了。

2. 数据建模与预处理:把 92 个网点变成一张可计算的配送时间矩阵

这道题最容易被低估的环节,反而在正式建模之前。很多同学拿到附件先急着写聚类代码,结果后面所有方案都建立在错误的距离上。我先讲清楚三个数据表怎么用,再给出完整的预处理流程。

2.1 附件数据结构拆解:坐标、订奶量、道路表各承担什么角色

附件里其实有三类信息。第一类是网点坐标表,每一行包含网点编号、横坐标、纵坐标,它决定空间位置;第二类是订奶量表,每一行给出某个网点的当日需求,它决定这个网点在选址中的权重;第三类是配送道路表,记录相邻两个网点之间的道路长度,它决定实际行驶距离。

这里有个关键点:题目给的是道路数据,不是让你直接算欧氏距离。92 个网点之间的直线距离再近,如果中间没有道路连通,配送车也得绕行。所以必须把道路表先构造成一个带权无向图,再用最短路径算法算出任意两个网点之间的最短路。注意配送道路默认是双向的,起点和终点互换都能走;如果题目明确有单行道,建模时就把邻接矩阵改成不对称的。

订奶量的权重意义也要提前想清楚。后面无论是 K-Means 聚类还是整数规划,需求量大的网点对站点位置的影响理应更大。一个每天订 300 瓶的便利店和一个每天订 30 瓶的小报亭,在选址决策中不能同等对待。所以预处理阶段就要把订奶量单独存成一个权重向量,后面的距离矩阵和需求向量永远是分开的两个变量。

还有一个小细节:检查道路表是否连通。实际数据里很可能存在孤立网点,也就是没有道路和其他网点相连。这种点在配送网络里无法通过车辆到达,如果不处理,集合覆盖模型会直接判定无解。我的处理办法是:若两点之间没有直达道路,就先查找间接连通路径;如果整个图都不是连通图,就给孤立点补充一条到最近网点的虚拟道路,或单独说明这个网点需要特殊配送方案。

2.2 构建最短路矩阵:Floyd-Warshall 算法与 20km/h 时速换算

拿到坐标和道路表后,第一步是把道路表展开成邻接矩阵。92 个网点就是 92 个节点,用一个 92×92 的矩阵存任意两点间的道路距离,没有直达道路的填一个大数(比如无穷大),自身到自身填 0。接下来用 Floyd-Warshall 算法跑一遍,得到任意两网点之间的最短道路距离。

import pandas as pd import numpy as np # 读入网点表和道路表 points = pd.read_excel("网点信息.xlsx") # 列:网点编号、x、y、订奶量 roads = pd.read_excel("配送道路.xlsx") # 列:起点、终点、道路距离 n = len(points) # 初始化邻接矩阵:无穷大表示不可达 dist = np.full((n, n), np.inf) np.fill_diagonal(dist, 0) # 将道路表写入邻接矩阵,注意双向写入 for _, r in roads.iterrows(): i, j = int(r["起点"]), int(r["终点"]) length = r["道路距离"] if length < dist[i, j]: dist[i, j] = length dist[j, i] = length # Floyd-Warshall 求解任意两点最短道路距离 for k in range(n): dist = np.minimum(dist, dist[:, k][:, None] + dist[None, k, :]) # 将距离(km)转换为配送时间(分钟),车速 20km/h time_matrix = dist / 20.0 * 60

这段代码里的距离矩阵dist是后续所有模型的基础。Floyd-Warshall 的时间复杂度是 O(n³),92 个节点大约 78 万次运算,普通笔记本瞬间就能跑完。之所以不用 Dijkstra,是因为这里需要的是“所有点对”之间的最短路径,Floyd 实现最简单,而且矩阵化写法在 NumPy 里非常快。

转换时间时要注意单位:车速 20km/h,道路距离单位是公里,那么时间(分钟)= 距离 / 20 × 60。这一步很多人会在单位上翻车,直接拿距离当时间用,导致后面的 10 分钟约束完全失真。建议代码里加一行断言,打印最大和最小时间,肉眼检查数量级是否合理。

到这里,原始的空间距离已经变成了“配送时间矩阵”。这道题的精髓就在于所有约束都必须基于这个时间矩阵来谈,而不是基于坐标算出来的直线距离。

2.3 配送可达性初步观察:3.33km 半径下的覆盖缺口

预处理做完后,先别急着建模。花五分钟画两张图:一张是 92 个网点的散点图,另一张是订奶量直方图。这两张图能帮你判断后面的建模方向。

散点图能直观看出网点是不是天然分成几个片区。如果网点集中在三个方向,那最终配送站数量大概率不会少于三个。订奶量直方图则告诉你需求分布是否悬殊。如果前五个网点占了一半需求量,那最优选址方案里,这几个网点附近必须设站,否则配送车一天要跑很多趟。

再把 10 分钟时效约束粗算一下:20km/h 的车速下,10 分钟最多跑 3.33km。拿这个半径去看配送网络,哪些网点之间互相可达、哪些网点周边 3.33km 内没有其他网点,一目了然。如果某网点周边道路稀疏,它几乎必然要成为候选配送站,这种先验信息在答辩时很能加分。

这一章做出的时间矩阵time_matrix和需求向量demand,就是后续所有模型的输入。接下来正式进入三个子问题的建模。

3. 问题一建模与求解:经济成本目标下的配送站选址方案

问题一没有硬性时效约束,只要求综合考虑配送速度、配送成本和建设成本,在 92 个网点位置中选一个或多个点建站。这个描述其实是设施选址问题的标准形态,难点在于成本函数的拆解和站点数量的确定。

3.1 成本函数设计:建设成本、配送变动成本与时间惩罚系数

我的做法是把总成本拆成三块:建设成本、运输成本、超时惩罚成本。先把它们量化,才能比较一个站和五个站哪个更经济。

建设成本比较好理解:每个配送站需要场地、冷藏柜、人员,折算到每天是一个固定值。用c_build表示单站每日建设运营成本,建 k 个站就是k × c_build。注意题目没有给具体金额,所以答案里应该体现“系数可变”的敏感度分析思路,而不是硬报一个金额。

运输成本是变动成本,它和配送距离、配送量都成正比。网点 i 的需求量记为w_i,如果它被分配给配送站 j,那么运输成本就是β × w_i × dist(i,j),其中 β 是单位距离单位需求量的运输成本系数。这里注意:运输成本应该用“需求量×距离”的加权形式,因为同样一公里的路,送得多和送得少,消耗的运力完全不同。

超时惩罚是个可选部分。题目说鲜奶保鲜要求尽量缩短在途时间,这个约束没有一个明确的分钟阈值,但如果某条配送路线明显过长,就相当于隐性损失。我的处理是设置一个时间惩罚项γ × max(0, t_ij - T_target),其中T_target是自己定义的目标时间(比如 30 分钟),超过部分线性惩罚。这样模型在优化成本的同时,会自然避开那些配送时间异常长的分配方案。

综合后目标函数写成:

min Σ_{j∈S} c_build_j + β × Σ_i Σ_j w_i × dist(i,j) × x_ij + γ × Σ_i Σ_j max(0, t_ij - T_target) × x_ij

约束条件是每个网点恰好分配到一个配送站:Σ_j x_ij = 1;只有建站点才能接收分配:x_ij ≤ y_j,其中y_j ∈ {0,1}表示是否在 j 点建站;候选站集合限定在 92 个网点位置中。

这个模型本质上是一个 P-Median 问题的变形。求解思路是先假设建 k 个站,对 k 从小到大扫描,找出总成本的边际变化,再决定最优站点数。

3.2 带权聚类与整数规划的两步法求解

直接对 92 个候选点做整数规划,变量不算多,但要反复试 k,效率不高。我的习惯是先做一个快速聚类确定“合理分区”,再对每个簇内做精确优化。具体分两步:

第一步,用订奶量作为样本权重做 K-Means 聚类。为什么带权?因为需求量大的网点应当在聚类时吸引中心点靠近自己,这样运输成本更低。K-Means 得到的质心是一个连续坐标,不一定落在某个网点上,但它能反映出这个片区的配送中心应该在哪个方位。

第二步,把每个簇的质心坐标“吸附”到簇内成本最低的候选网点上。也就是枚举该簇所有网点作为候选配送站,计算该簇内所有需求点到候选站的总运输成本,选最小的那个网点作为最终配送站。

from sklearn.cluster import KMeans # coords: 92x2 的网点坐标, demand: 92 维订奶量 kmeans = KMeans(n_clusters=k, init="k-means++", n_init=10, random_state=42) labels = kmeans.fit_predict(coords, sample_weight=demand) # 对每个簇,在簇内选择总成本最低的网点作为配送站 def best_site_in_cluster(cluster_idx, coords, demand, time_matrix): members = np.where(labels == cluster_idx)[0] best_site, best_cost = None, np.inf for candidate in members: # 簇内所有网点到候选站的时间/距离成本 cost = np.sum(demand[members] * time_matrix[candidate, members]) if cost < best_cost: best_cost = cost best_site = candidate return best_site, best_cost

这段代码里,sample_weight=demand是关键。K-Means 默认把所有样本看得一样重要,这在选址问题里会出大问题:一个高需求网点可能被分到离站点很远的地方,运输成本暴涨。加上权重后,聚类中心会主动向高需求点靠拢。

第二步的枚举式吸附也很重要。K-Means 质心是连续坐标,但在本题中配送站只能建在网点上。直接取质心坐标是无效解,正确的做法是枚举该簇内的候选网点,把质心落到真实可行的位置。这一步同时也解决了“质心落在马路中间”的尴尬。

3.3 站点数量扫描:从 1 站到多站的边际成本变化

现在的问题是 k 到底取几。我倾向于写一个循环,让 k 从 1 扫描到 10,每次跑上面的两步法,记录总成本。成本曲线的形状通常是先快速下降,然后趋于平缓,甚至因为建设成本上升而掉头向上。最优 k 就是边际成本递减到接近 0 的那个拐点。

以赛题常见的城市网点分布来看,单站方案往往不是最优。一个配送站虽然省了建设成本,但所有网点都要由它配送,车辆行驶距离总和非常可观。随着 k 增加,运输成本快速下降;当 k 超过某个值后,再多建一个站省下的运输成本已经小于它的建设成本,这时候总成本开始回升。

我用一个示意表格展示这种趋势(具体数字随附件数据变化,这里只演示判断逻辑):

站点数 k日均运输成本日均建设成本总成本
11520080016000
2820016009800
3540024007800
4420032007400
5360040007600

这个表的意思是:当 k=4 时总成本最低,k=5 以后边际运输成本下降已经盖不住额外建设成本,所以问题一的推荐方案是 4 个配送站。当然,这只是判断逻辑的演示,实际值要回到题目附件数据里去计算。

4. 问题二与问题三建模:10 分钟时效约束下的最少站点数与配送量均衡

问题二开始加硬约束:配送时间不得超过 10 分钟,车速 20km/h。这意味着每辆车只能覆盖半径 3.33km 以内的区域。问题三进一步要求配送量均衡:一个配送站分到的订奶量不能太低导致资源浪费,也不能太高导致配送延误。这两问本质上是同一个问题的两个约束维度,我放在一章里一起建模。

4.1 集合覆盖模型:10 分钟约束等价于 3.33km 覆盖半径

10 分钟约束可以很自然地转换成覆盖关系:以候选配送站 j 为中心,凡是送达时间不超过 10 分钟的网点 i,都算被 j 覆盖。由于车速固定是 20km/h,覆盖半径就是10/60 × 20 = 3.33km。注意这个判断必须用上一章算出的道路最短路矩阵,不能用欧氏距离。

我们要回答的第一个问题是:至少需要几个配送站才能覆盖所有网点?这是经典集合覆盖问题(Set Covering Problem),整数规划模型非常简单:

import pulp n = len(points) R = 3.33 # 覆盖半径,单位 km # 覆盖矩阵:cover[i,j]=1 表示网点 i 被网点 j 的配送站覆盖 cover = (dist <= R).astype(int) # 集合覆盖模型:最小化站点数量 prob = pulp.LpProblem("MinStationCover", pulp.LpMinimize) x = [pulp.LpVariable(f"x_{j}", cat="Binary") for j in range(n)] prob += pulp.lpSum(x) for i in range(n): prob += pulp.lpSum(x[j] * cover[i, j] for j in range(n)) >= 1 prob.solve(pulp.PULP_CBC_CMD(msg=False)) selected = [j for j in range(n) if pulp.value(x[j]) > 0.5] print("最少需要配送站数量:", len(selected)) print("选定网点编号:", selected)

这个模型把所有网点都当作候选建站位置,约束条件保证每个网点至少被一个站覆盖。目标函数是建站数量最小。

求解之后要注意一件事:集合覆盖模型求出的站点数是最少数量,但不一定是经济最优。比如某个候选点单独覆盖了四个网点,但它本身订奶量很低,建在那里会导致配送量过小。所以我在求解后还会加一个二次筛选:如果存在多个同样站点数的最优解,则选择运输总距离(加权)最小的那个。方法是在目标函数里给每个变量加一个极小的运输成本系数,或者把集合覆盖的解作为初始解,再跑一遍带成本目标的下界优化。

我通常的做法是:先在目标函数里为每个候选站点 j 加一个权重项ε × Σ_i w_i × dist[i,j] × cover[i,j],表示“在同样覆盖数量下优先选对运输成本贡献小的站点”。ε 设成 0.001 之类的小数,不会改变最优站点数,但能打破对称性,让求解器选出一个运输成本更低的解。

4.2 固定站点数下的配送量均衡:变异系数 CV 与聚类目标改造

问题三在问题二的基础上增加配送量均衡约束。这里要澄清一点:均衡不是让每个配送站分到的订奶量完全一样,而是避免极端不平衡。衡量均衡的常用指标是变异系数 CV,即各站配送量的标准差除以平均值。CV 越小,配送量分布越均匀。

建模时我采用双目标:总运输成本要尽量小,各站配送量差异也要尽量小。把这两个目标合成一个带惩罚的模型:

min β × Σ_i Σ_j w_i × dist(i,j) × x_ij + λ × Σ_j (W_j - W_avg)²

其中W_j = Σ_i w_i × x_ij是分配到站点 j 的总订奶量,W_avg = 总订奶量 / 站点数。λ 是均衡系数,调整它就能在“运输成本优先”和“配送量均衡优先”之间滑动。

这个模型是非线性的,因为第二项包含了x_ij的乘积。实际求解时更稳定的做法是把分配模型写成带约束的整数规划:设定一个阈值Δ,要求每个站点的配送量落在[W_avg - Δ, W_avg + Δ]区间内,然后在这个约束下最小化运输成本。Δ 从大往小试,找到一个可行解里运输成本增加不超过 5% 的最小 Δ。

# 分配模型:固定站点数量 k,带配送量均衡约束 W_avg = total_demand / k delta = 0.15 * W_avg # 初始允许 ±15% 波动 prob = pulp.LpProblem("BalancedAssignment", pulp.LpMinimize) a = [[pulp.LpVariable(f"a_{i}_{j}", cat="Binary") for j in sites] for i in range(n)] # 每个网点恰好分配到一个站 for i in range(n): prob += pulp.lpSum(a[i][j] for j in range(len(sites))) == 1 # 各站配送量不得偏离均值过多 for t, j in enumerate(sites): W_j = pulp.lpSum(demand[i] * a[i][t] for i in range(n)) prob += W_j <= W_avg + delta prob += W_j >= W_avg - delta # 最小化加权运输距离 prob += pulp.lpSum(demand[i] * time_matrix[i, j] * a[i][t] for t, j in enumerate(sites) for i in range(n)) prob.solve(pulp.PULP_CBC_CMD(msg=False))

实际运行时,如果 Δ 设得太小,模型可能无解,因为某些网点离某一个站特别近,必然被分给它,而它的总需求量又恰好很高。这时就体现出“均衡”是一个和运输成本换取的目标,不是绝对平均。我一般会记录不同 Δ 下的总运输成本变化,画一条折线交给老师看,比单纯报一个 CV=0.05 的数字更有说服力。

4.3 双目标权衡与方案对比:把三个问题统一到同一套框架

三个问题其实是层层递进的:问题一给出经济最优站点数,问题二用 10 分钟约束修正站点数,问题三在修正后的站点数上做配送量均衡。最后呈现时赛题答案,我会给出一个对比表:

方案类型站点数最大配送时间平均配送时间负载极差CV
问题一经济最优428min9.2min高大0.72
问题二时效约束99.8min4.6min中0.51
问题三均衡优化99.5min5.1min缩小0.18
最终推荐方案99.5min5.1min均衡0.18

这个表的结构很直观:问题二把最大配送时间压到 10 分钟以内,问题三在保证时效的前提下把配送量拉平。最终方案我通常会在问题三的基础上做一次微调:把配送量最低的那个站合并到相邻站,再重新验证是否仍然满足 10 分钟约束。如果能满足,就少建一个站,节省建设成本。

到这一步,三个子问题已经全部建模完毕。但我在实际带项目的过程中发现,真正让方案翻车的往往不是模型本身,而是那些藏在数据细节里的坑。下面单独用一章讲最常见的五类问题。

5. 常见问题避坑:模型翻车往往发生在求解之前

这类选址建模项目里,最终结果跑出来不对劲,八成不是算法的错。以下五个问题,是我见过最多的翻车点,每条都按“现象、原因、解决”写清楚。

5.1 把欧氏距离当成实际配送距离,覆盖结果虚高

有些同学拿坐标直接算两点间距离,然后判断是否在 3.33km 内,结果出来 6 个站就能覆盖全部 92 个网点。但换成道路最短路之后,可能 9 个站都不够。原因很简单:城市道路是网状的,直线能到的地方,车辆往往要绕行。解决的办法是强制走道路表建立邻接矩阵,用 Floyd-Warshall 算出真正距离后再做覆盖判断。我在预处理阶段会加一句调试代码,打印“直距平均 / 路距平均”比例,通常这个比例在 1.2 到 1.8 之间,提醒自己两者差异有多大。

5.2 K-Means 的连续质心落在马路中间,直接拿去做站址

K-Means 出来的质心是一个浮点坐标,它大概率不会和任何网点坐标重合。如果把质心当成建站位置,就违背了“在 92 个网点中选择”的题目约束。解决方法是把质心看作区域参考位置,再在该簇所有网点中枚举一个总成本最低的真实网点作为配送站。要注意枚举时用的距离必须是路网距离矩阵,而不是欧氏距离。

5.3 负载均衡做到“绝对平均”,运输成本反而暴涨

有些同学把均衡约束设得很严格,比如要求每个站配送量都不超过均值 ±5%,结果网点分配严重偏离自然就近原则,很多本来应该配给 A 站的网点被强行分给 B 站,总运输成本翻了一倍。这说明均衡和运输成本是矛盾关系。解决办法是画出“CV vs 总运输成本”的帕累托曲线,找一个斜率拐点。通常 CV 从 0.5 压到 0.2 时运输成本增加很少,但从 0.2 压到 0.05 时运输成本会飙升,那就不值得。

5.4 只算路网时间,忽略装车和卸货时间

题目给的车速只覆盖行驶过程,但配送站装车、网点卸货也是要时间的。如果一个网点订奶量很大,车辆停靠卸货可能需要 3 到 5 分钟。用 10 分钟作为纯行驶时间约束,得到的最优方案在实际执行时可能会超时。解决方法是把订奶量折算成停靠时间,比如每 100 瓶折算 1 分钟,加进时间矩阵后再做覆盖判断。这个细节在答辩时提出来,会显得你的方案更贴近实际。

5.5 整数规划求解器每次运行结果不同,方案不稳定

集合覆盖模型如果不加随机种子,不同机器甚至同机器不同批次运行,可能给出不同数量的站点解。原因是 CBC 这类开源求解器在分支定界过程中带有随机扰动。解决方法是固定随机种子,并让模型存在多个可选解时,用“覆盖总量加权后的运输成本最小”作为二次目标来消解对称性。答辩前务必把random_state或seed固定下来,保证演示结果可复现。

6. 模型验证与答辩准备:用敏感性分析和三张图让结论站得住

最后一步不是写代码,而是让方案能被验证、被讲解。我用到的核心方法是敏感性分析,以及把结果浓缩成三张图。

敏感性分析要回答三个问题:车速如果改成 15km/h 或 25km/h,最优站点数量是否变化;建设成本系数变化 50%,经济最优站点数是否还落在同一个值;10 分钟约束改成 8 分钟或 12 分钟,站点数增减是否合理。做法很简单,把关键参数分别乘上 0.75、1.0、1.25,重新跑一遍模型,记录站点数和总成本变化。如果站点数波动只在 1 到 2 个站之间,说明方案是稳健的;如果从 4 个站直接跳到 13 个站,说明模型对参数极端敏感,答辩时要主动解释原因。

答辩演示我强烈建议只放三张图。第一张是“站点数-总成本曲线”,用来支撑问题一的经济选址结论;第二张是“10 分钟覆盖范围图”,用不同颜色画出每个配送站覆盖的网点区域,直观证明所有网点都在时效范围内;第三张是“各站配送量柱状图”,展示问题三的均衡效果。这三张图能覆盖三个问题,每张都要求对应一个明确结论,不要放一大堆中间过程的曲线图。

还有两个答辩时的高频问题要提前准备。如果被问“为什么不用重心法”,就回答:重心法适用于站址可以任意取、道路均匀的理想平面,本题已经限定候选位置在 92 个网点中,且配送必须走给定道路网络,所以采用整数规划更符合约束条件。如果被问“均衡系数 λ 是怎么定的”,就回答:λ 不是拍脑袋定的,而是通过扫描 λ 值观察 CV 和运输成本的权衡曲线,选取边际效益明显下降的点。

从那次带完这道建模题之后,我养成了一个雷打不动的习惯:凡是做选址类方案,先花 30 分钟把距离矩阵和约束条件验算一遍,再谈用什么高级算法。很多复杂问题翻车,翻得根本不是算法,而是基础数据没对清楚。希望这套思路能帮你在答辩时少踩一个坑。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进