设施选址问题与模型——Python实现
设施选址问题(Facility Location Problem, FLP)也称为选址-分配问题,是运筹学中非常经典的内容。该问题是指在确定选址对象,选址目标区,成本函数以及约束条件的前提下,以总物流成本最低或总服务水平最优或社会效益最大化为目标,确定物流系统中物流节点的数量,位置,从而合理规划物流网络结构。选址问题在公司实现其经营战略与目标的过程中有着重大的意义。选址的好坏直接影响到生产成本,服务时间,服务质量等等,进而影响公司的利润与其在商业市场上的竞争力。一个好的选址可以减少生产成本,节省顾客购买时间,便利顾客,而差的选址会带来物流中的不便与损失。由于选址是一项长期性投资,一旦确定下来就不能随意更改,因此规划其位置前必须进行深入的调查和周密的考虑。
一、选址理论方法
选址理论的研究,最早始于1909 年,Weber 研究如何在平面上确定一个仓库位置,使仓库与顾客间的总距离最小(也称为 韦伯问题) ;而后在1964 年,Hakimi 提出了网络上的p-中值问题与p-中心问题,该研究在选址问题上具有里程碑意义,此后更多学者加入到选址理论的研究中。目前选址问题主要有三类:
① P-中位问题(也称P-中值问题):研究如何选择P个服务站使得需求点和服务站之间的距离与需求量的乘积之和最小,在物流领域应用得非常广泛;
②P-中心问题(也称 minmax 问题):是探讨如何在网络中选择 P 个服务站,使得任意一需求点到距离该需求点最近的服务站的最大距离最小问题,在应急设施的选址上应用较广泛,如警局、消防局、医院等公共服务的选址,要求尽可能快到达;
③ 覆盖问题分为最大覆盖问题和集覆盖问题两类,集覆盖问题研究满足覆盖所有需求点顾客的前提下,服务站总的建站个数或建设费用最小的问题;最大覆盖问题是研究在备选物流中心里,如何选择p个设施,使得服务的需求点数最多或需求量最大。
![]() |
![]() |
二、数学模型
选址问题,旨在选址目标区内确定设施的位置以及数量,并满足一定的约束条件,使得目标最优。选址问题根据规划区域,距离,目标有多种分类。
2.1 P-中值模型
P-中值模型是指在一个给定数量和位置的需求集合和一个候选设施位置的集合下,分别为个设施找到合适的位置,并指派每个需求点到一个特定的设施,使之达到在工厂和需求点之间的运输费用最低。P-中值模型一般适用于在工厂或者仓库的选址问题,例如要求在它们和零售商或者顾客之间的费用最少。中值模型是以用户到最近设施的平均距离或者总距离最小的方式,确定固定数量设施的位置。经典中值模型的基本假设是不管需求有多少,每个设施都有足够的资源来满足需求,从而假设每个人都能用离其最近的设施。引入需求加权的平均距离或者总距离,对于解决以成本和收益为目标的选址问题十分有效。
P-中值模型可以通过精确的数学语言进行描述,要求准确的表达问题的约束条件、目标以及合理的变量定义。
- 变量定义
- 目标函数
- 约束条件
(c-1) 每个客户由一个设施服务,保证每个客户(需求点)只有一个设施来提供相应的服务
(c-2) 设施总数限制,限制总的设施数目为 个
(c-3) 设施与服务的对应关系,有效得保证没有设施的地点不会有客户对应
- 数学模型
- :在研究对象中的个客户
- :在研究对象中的个拟建设施的候选地点
- :第 个客户的需求量
- :从地点到地点的单位运输费用
- :可以建立的设施总数
- :如果在建立设施,则 ;否则为 0
- :如果客户 由设施 来提供服务时,则其为 1;否则为 0
- 问题求解
求解 P-中值模型需要解决两个方面的问题:选择合适的设施位置(数学表达式中的变量 );指派客户到相应的设施中去(数学表达式中的变量 ),一旦设施的位置确定之后,再确定每个客户到不同的设施中,使费用总和 最小就十分简单了。 - P-中值算法
P-中值模型是 NP-hard 问题,一般启发式算法【贪婪取走启发式算法,Greedy Dropping Heuristic Algorithm】用来求解该问题。
令当前选中设施点数 ,即将所有 个候选位置都选中;
将每个客户指派给 个设施点中距离最近的一个设施点,求出总费用 ;
若 满足条件,则输出 个设施点以及各客户的指派结果;否则,转第 4 步;
从 个设施候选点中选择一个取走点,该点要满足将它取走并将它的客户指派给其他的最近设施点后总费用增加量最小,从候选集合中删去刚刚确定的取走点,令 ,转第 2 步。
2.2 p-中心 (p-center) 模型
-中心问题与 中值问题的唯一不同点在于目标函数的形式。在 中心问题中,目标函数为 MinMax形式,目标是使每个需求点到最近设施的最大距离最小,通常用于消防站,警察局等应急设施的选址中。如消防站设施选址问题中,目标为使区域内的每个用户都能在某个阈值时间内得到消防服务,在满足约束的条件下,该阈值越小越好。
2.3 集合覆盖模型
集合覆盖模型是指在覆盖所有需求点的前提下,使得总建设费用最低,当每个设施的建设费用相同时,问题简化为枢纽数目最少,该模型可以表示为:
其中, 表示所有需求点的集合, 表示第 个需求点的需求量, 表示设施 的服务限制, 表示设施 所服务的需求点 的集合, 表示物流中心的集合, 如果 被选为设施, 否则 表示设施 满足需求点 的需求量与 的比值。
2.4 重心法(Centroid Method)
重心法是解决设施选址问题的一种常用方法,特别适用于在平面上选址,使得到各需求点的加权距离最小。假设要建立一个配送中心以向个零售商供货,零售商在平面上的坐标为,各零售商的装运量分别为。重心法给出的配送中心位置的公式如下:
# 各个供应地的坐标和运输量,然后利用重心法公式计算出配送中心的位置
# 这里假设坐标,实际应用时需要根据实际坐标替换
coordinates = {
'P1': {'x': 20, 'y': 70, 'q': 2000},
'P2': {'x': 60, 'y': 60, 'q': 1200},
'P3': {'x': 20, 'y': 20, 'q': 1000},
'P4': {'x': 50, 'y': 20, 'q': 2500},
}
# 计算总运输量
total_quantity = sum(point['q'] for point in coordinates.values())
# 计算重心坐标
x_center = sum(point['x'] * point['q'] for point in coordinates.values()) / total_quantity
y_center = sum(point['y'] * point['q'] for point in coordinates.values()) / total_quantity
# 输出结果
print(f"配送中心的最优位置为: ({x_center:.2f}, {y_center:.2f})")
#配送中心的最优位置为: (38.36, 42.09)
三、选址问题建模与求解--Python分析
3.1 p-中值选址问题建模与求解
#带容量约束的p-中值选址问题建模与求解
#需求点位置及需求量,备选中心位置及能力
demandCoordinates = [(88, 16),(25, 76),(69, 13),(73, 56),(80, 100),(22, 92),(32, 84),(73, 46),(29, 10),(92, 32),(44, 44),(55, 26),(71, 27),(51, 91),(89, 54),(43, 28),(40, 78)]
centerCoordinates = [(32, 60),(69, 33),(49, 40),(72, 81),(61, 65)]
d = [3,4,5,6,7,4,2,3,4,5,6,3,5,4,3,5,1] #需求量,对应demandCoordinates
c = [25,25,25,25,25] #能力都设置为25,对应centerCoordinates
![]() |
![]() |
# -*- coding: utf-8 -*-
import math
import random
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.pylab import mpl
mpl.rcParams['font.sans-serif'] = ['SimHei'] # 添加这条可以让图形显示中文
def cal_fitness(chrom, dis_matrix, cnum, demand, capacity):
"""
计算路径距离,即评价函数
"""
dis_sum = 0
declist = [i for i in range(cnum) if chrom[i] == 1]
d_list = [[] for _ in range(cnum)]
weight = [0 for _ in range(cnum)]
total_demand = [0 for _ in range(cnum)]
for i in range(cnum, len(chrom)):
dec_chrom = declist[chrom[i] - 1]
d_list[dec_chrom].append(i - cnum)
weight[dec_chrom] += dis_matrix.loc[i - cnum, dec_chrom] * demand[i - cnum]
total_demand[dec_chrom] += demand[i - cnum]
for i in range(len(total_demand)):
if total_demand[i] > capacity[i]:
return math.pow(10, 10)
return round(sum(weight), 1)
def traversal_search(chrom, dis_matrix, tabu_list, cnum, demand, capacity, p, traversal_max):
"""
邻域随机遍历搜索,成对交换+单点变异
"""
traversal_list = []
traversal_value = []
for _ in range(traversal_max):
new_chrom = chrom.copy()
pos1, pos2 = random.sample(range(cnum), 2)
new_chrom[pos1], new_chrom[pos2] = new_chrom[pos2], new_chrom[pos1]
pos1, pos2 = random.sample(range(cnum, len(chrom)), 2)
new_chrom[pos1], new_chrom[pos2] = new_chrom[pos2], new_chrom[pos1]
for i in range(cnum, len(chrom)):
if random.random() > 0.75:
new_chrom[i] = random.randint(1, p)
new_value = cal_fitness(new_chrom, dis_matrix, cnum, demand, capacity)
if new_chrom not in traversal_list and new_chrom not in tabu_list:
traversal_list.append(new_chrom)
traversal_value.append(new_value)
min_value = min(traversal_value)
return min_value, traversal_list[traversal_value.index(min_value)]
def initialize(dnum, cnum, p):
"""
初始化染色体
"""
clist = random.sample(range(cnum), p)
cchrom = [1 if i in clist else 0 for i in range(cnum)]
dchrom = [random.randint(1, p) for _ in range(dnum)]
return cchrom + dchrom
def draw_sca(demand_coordinates, center_coordinates):
"""
画散点图
"""
plt.scatter(*zip(*demand_coordinates), color='#ff69E1', marker='o')
plt.scatter(*zip(*center_coordinates), color='#4169E1', marker='*')
plt.xlabel('x')
plt.ylabel('y')
plt.show()
def draw_path(chrom, demand_coordinates, center_coordinates, p, cnum):
"""
画分布图
"""
centerlist = [center_coordinates[i] for i in range(cnum) if chrom[i] == 1]
for i in range(cnum, len(chrom)):
plt.plot(
[centerlist[chrom[i] - 1][0], demand_coordinates[i - cnum][0]],
[centerlist[chrom[i] - 1][1], demand_coordinates[i - cnum][1]],
color='#4169E1', alpha=0.8, linewidth=0.8
)
draw_sca(demand_coordinates, center_coordinates)
if __name__ == '__main__':
CityNum = 50
MinCoordinate = 0
MaxCoordinate = 100
# TS参数
tabu_limit = 100
iter_max = 1000
traversal_max = 100
tabu_list = []
tabu_time = []
best_value = math.pow(10, 10)
best_line = []
# 需求点和备选中心位置及需求量和能力
demand_coordinates = [(88, 16), (25, 76), (69, 13), (73, 56), (80, 100), (22, 92), (32, 84), (73, 46), (29, 10), (92, 32), (44, 44), (55, 26), (71, 27), (51, 91), (89, 54), (43, 28), (40, 78)]
center_coordinates = [(32, 60), (69, 33), (49, 40), (72, 81), (61, 65)]
demand = [3, 4, 5, 6, 7, 4, 2, 3, 4, 5, 6, 3, 5, 4, 3, 5, 1]
capacity = [25, 25, 25, 25, 25]
draw_sca(demand_coordinates, center_coordinates)
p = 3
dnum = len(demand_coordinates)
cnum = len(center_coordinates)
dis_matrix = pd.DataFrame(
[[round(math.sqrt((demand_coordinates[i][0] - center_coordinates[j][0])**2 + (demand_coordinates[i][1] - center_coordinates[j][1])**2), 2) for j in range(cnum)] for i in range(dnum)]
)
num = 50
chroms = [initialize(dnum, cnum, p) for _ in range(num)]
values = [cal_fitness(chrom, dis_matrix, cnum, demand, capacity) for chrom in chroms]
best_value = min(values)
best_chrom = chroms[values.index(best_value)]
chrom, value = best_chrom, best_value
print('初代最优值 %.1f' % (best_value))
best_value_list = [best_value]
tabu_list.append(best_chrom)
tabu_time.append(tabu_limit)
itera = 0
while itera <= iter_max:
new_value, new_chrom = traversal_search(chrom, dis_matrix, tabu_list, cnum, demand, capacity, p, traversal_max)
if new_value < best_value:
best_value, best_chrom = new_value, new_chrom
best_value_list.append(best_value)
print('第%d代最优值 %.1f' % (itera, best_value))
chrom, value = new_chrom, new_value
tabu_time = [x - 1 for x in tabu_time]
if 0 in tabu_time:
index = tabu_time.index(0)
tabu_list.pop(index)
tabu_time.pop(index)
tabu_list.append(chrom)
tabu_time.append(tabu_limit)
itera += 1
draw_path(best_chrom, demand_coordinates, center_coordinates, p, cnum)
#第1000代最优值 1645.2
3.2 p-中心选址问题建模与求解
p-中心问题与p-中位问题都是在备选的设施点集合里选p个点。最主要的差别是目标函数,这也决定了他们的应用场景不同。 p-中心问题是希望最小化每个需求点到设施的距离的最大距离,这样不会出现有些距离很大有些距离很小,相对平衡;p-中位问题是希望带权重的距离总和最小。
import pandas as pd
import numpy as np
import pyscipopt as opt
import random
def p_center_problem():
# ==========测试数据==========
num_nodes = 17
num_facilities = 5
P = 3 # 待决策设施数量
# 需求点位置及需求量
demandCoordinates = [(88, 16), (25, 76), (69, 13), (73, 56), (80, 100),
(22, 92), (32, 84), (73, 46), (29, 10), (92, 32),
(44, 44), (55, 26), (71, 27), (51, 91), (89, 54),
(43, 28), (40, 78)]
d = [3, 4, 5, 6, 7, 4, 2, 3, 4, 5, 6, 3, 5, 4, 3, 5, 1] # 需求量
# 备选中心位置及能力
centerCoordinates = [(32, 60), (69, 33), (49, 40), (72, 81), (61, 65)]
c = [25, 25, 25, 25, 25] # 能力
# 计算需求点和备选中心之间的距离矩阵
distance_matrix = np.zeros((num_nodes, num_facilities))
for i in range(num_nodes):
for j in range(num_facilities):
distance_matrix[i, j] = np.sqrt((demandCoordinates[i][0] - centerCoordinates[j][0]) ** 2 +
(demandCoordinates[i][1] - centerCoordinates[j][1]) ** 2)
# ==========建立模型==========
model = opt.Model('p-Center Problem')
# ==========定义变量==========
facility = {}
for j in range(num_facilities):
facility[j] = model.addVar(vtype='B', name='facility_' + str(j))
assign_serve = {}
for i in range(num_nodes):
for j in range(num_facilities):
assign_serve[i, j] = model.addVar(vtype='B', name='node_' + str(i) + '_facility_' + str(j))
max_distance = model.addVar(vtype='C', name='max_distance')
# ==========定义约束==========
# 约束1: 选择的设施的总数量 = P
model.addCons(opt.quicksum(facility[j] for j in range(num_facilities)) == P)
# 约束2: 每个需求点都必须被唯一一个设施点满足
for i in range(num_nodes):
model.addCons(opt.quicksum(assign_serve[i, j] for j in range(num_facilities)) == 1)
# 约束3: 如果需求点i被设施点j满足,则代表设施点j被选择。反之亦然
for i in range(num_nodes):
for j in range(num_facilities):
model.addCons(facility[j] >= assign_serve[i, j])
# 约束4: 计算每个需求点最大距离
for i in range(num_nodes):
model.addCons(opt.quicksum(distance_matrix[i][j] * assign_serve[i, j] for j in range(num_facilities)) <= max_distance)
# ==========定义目标==========
# 目标: 最小化每个需求点的最大距离
model.setObjective(max_distance)
model.setMinimize()
# ==========求解模型==========
model.optimize()
select_facility = []
for i in range(num_facilities):
if model.getVal(facility[i]) > 0:
select_facility.append(i)
assign_node_facility = []
for i in range(num_nodes):
for j in range(num_facilities):
if model.getVal(assign_serve[i, j]) > 0:
assign_node_facility.append((i, j))
# 输出结果
print(f'Objective value = {model.getObjVal()}')
print(f'Selected facilities = {select_facility}')
print(f'Node to facility assignments = {assign_node_facility}')
# 解释结果
print("\n解释结果:")
print(f"目标值 (Objective value) = {model.getObjVal()} 表示最小化后的最大距离,即所有需求点到其分配的设施点的最大距离。")
print(f"选择的设施点 (Selected facilities) = {select_facility} 表示被选择的 {P} 个设施点的索引。")
print(f"需求点到设施点的分配 (Node to facility assignments) = {assign_node_facility} 表示每个需求点被分配到的设施点的索引。")
if __name__ == '__main__':
# python调用SCIP求解p-中心选址问题
p_center_problem()
解释结果:
目标值 (Objective value) = 45.79301256742124 表示最小化后的最大距离,即所有需求点到其分配的设施点的最大距离。
选择的设施点 (Selected facilities) = [0, 2, 4] 表示被选择的 3 个设施点的索引。
需求点到设施点的分配 (Node to facility assignments) = [(0, 2), (1, 2), (2, 2), (3, 2), (3, 4), (4, 4), (5, 0), (6, 0), (7, 1), (7, 4), (8, 2), (9, 2), (10, 4), (11, 2), (12, 4), (13, 2), (13, 4), (14, 4), (15, 2), (16, 0)] 表示每个需求点被分配到的设施点的索引。
3.3 饲料公司p-中值选址
某饲料公司在某新地区经过一段时间的宣传广告后,得到了8个超市的订单。由于该新地区离总部较远,该公司拟在该地区新建2个仓库,用最低的运输成本来满足该地区的需求。经过一段时间的实地调查之后,已有4个候选地址。各候选地址到不同超市的运输成本、各个超市的需求量如下表所示。
候选地址到超市的运输成本表
超市i\选址j | 地址1 | 地址2 | 地址3 | 地址4 | 需求量 |
---|---|---|---|---|---|
1 | 4 | 12 | 20 | 6 | 100 |
2 | 2 | 10 | 25 | 10 | 50 |
3 | 3 | 4 | 16 | 14 | 120 |
4 | 6 | 5 | 9 | 2 | 80 |
5 | 18 | 12 | 7 | 3 | 200 |
6 | 14 | 2 | 4 | 9 | 70 |
7 | 20 | 30 | 2 | 11 | 60 |
8 | 24 | 12 | 6 | 22 | 100 |
mport pandas as pd
import numpy as np
import pyscipopt as opt
def p_median_problem():
# ==========输入数据==========
num_nodes = 8 # 超市数量
num_facilities = 4 # 备选仓库数量
P = 2 # 待决策设施数量
# 运输成本矩阵 (c_ij)
distance_matrix = np.array([
[4, 12, 20, 6],
[2, 10, 25, 10],
[3, 4, 16, 14],
[6, 5, 9, 2],
[18, 12, 7, 3],
[14, 2, 4, 9],
[20, 30, 2, 11],
[24, 12, 6, 22]
])
# 各超市的需求量 (d_i)
weight_demand = np.array([100, 50, 120, 80, 200, 70, 60, 100])
# ==========建立模型==========
model = opt.Model('p-Median Problem')
# ==========定义变量==========
facility = {}
for j in range(num_facilities):
facility[j] = model.addVar(vtype='B', name='facility_' + str(j))
assign_serve = {}
for i in range(num_nodes):
for j in range(num_facilities):
assign_serve[i, j] = model.addVar(vtype='B', name='node_' + str(i) + '_facility_' + str(j))
# ==========定义约束==========
# 约束1: 选择的设施的总数量 = P
model.addCons(opt.quicksum(facility[j] for j in range(num_facilities)) == P)
# 约束2: 每个需求点都必须被唯一一个设施点满足
for i in range(num_nodes):
model.addCons(opt.quicksum(assign_serve[i, j] for j in range(num_facilities)) == 1)
# 约束3: 如果需求点i被设施点j满足,则代表设施点j被选择。反之亦然
for i in range(num_nodes):
for j in range(num_facilities):
model.addCons(facility[j] >= assign_serve[i, j])
# ==========定义目标==========
# 目标: 最小化每个带权重需求点的总和
model.setObjective(opt.quicksum(weight_demand[i] * distance_matrix[i, j] * assign_serve[i, j] for i in range(num_nodes) for j in range(num_facilities)))
model.setMinimize()
# ==========求解模型==========
model.optimize()
select_facility = []
for i in range(num_facilities):
if model.getVal(facility[i]) > 0:
select_facility.append(i)
assign_node_facility = []
for i in range(num_nodes):
for j in range(num_facilities):
if model.getVal(assign_serve[i, j]) > 0:
assign_node_facility.append((i, j))
# 输出结果
print(f'Objective value = {model.getObjVal()}')
print(f'Selected facilities = {select_facility}')
print(f'Node to facility assignments = {assign_node_facility}')
# 解释结果
print("\n解释结果:")
print(f"目标值 (Objective value) = {model.getObjVal()} 表示最小化后的总运输成本,即所有需求点到其分配的设施点的加权运输成本总和。")
print(f"选择的设施点 (Selected facilities) = {select_facility} 表示被选择的 {P} 个仓库的索引。")
print(f"需求点到设施点的分配 (Node to facility assignments) = {assign_node_facility} 表示每个需求点被分配到的仓库的索引。")
if __name__ == '__main__':
# python调用SCIP求解p-中值选址问题
p_median_problem()
目标值 (Objective value) = 3740.0 表示最小化后的总运输成本,即所有需求点到其分配的设施点的加权运输成本总和。
选择的设施点 (Selected facilities) = [0, 2] 表示被选择的 2 个仓库的索引。
需求点到设施点的分配 (Node to facility assignments) = [(0, 0), (1, 0), (2, 0), (3, 0), (4, 2), (5, 2), (6, 2), (7, 2)] 表示每个需求点被分配到的仓库的索引。
总结
选址问题是物流、供应链管理等领域的关键问题,通常属于NP-Hard问题,传统的精确算法难以在合理时间内求得最优解。启发式算法成为求解此类问题的常用方法。常见的启发式算法包括遗传算法、蚁群算法和粒子群算法等。这些算法通过随机生成多个初始解,并通过特殊的迭代寻优规则进行优化,更新状态。当达到设定的迭代次数或其他终止条件时,算法结束,提供一个较优的可行解。启发式算法在处理大规模选址问题时具有求解时间短的优势,通常能得到较优解。然而,这些算法也存在容易陷入局部最优解的缺点,且求解过程与问题高度相关,改变问题结构对算法影响较大,难以扩展。对于大型、复杂的选址问题,仿真法是另一种有效的求解方法。仿真法通过重现系统活动,模拟现实情况来解决选址问题。这种方法通常需要对现实情况进行详细统计,获取一些关键参数的统计分布。与启发式算法相比,仿真法对计算机算力要求较小,不需要严格的编程,因而在一定程度上更为灵活和实用。然而,仿真法对实践的要求较高,准确的统计数据和参数分布是其成功的关键。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
2022-06-26 中心极限定理的模拟—R实现
2022-06-26 随机分布和随机数生成——R语言