医用口罩共享方案设计与优化
医用口罩共享方案设计与优化
由于新冠肺炎疫情影响,医用口罩的需求量日益增加。某中学班主任了解到班里 20 名学生的家庭所在小区每周均发放到家庭一定数量的免费医用口罩,于是班主任想到了一个口罩共享方案。通过联系各个家庭,了解到每个学生家庭每周拥有的口罩数量以及最低需求口罩数量,有些家庭是口罩数量不足,而有些家庭是有充足的口罩,如表 1 所示。假设这 20 个家庭每周拥有固定口罩总量及固定需求总量。
表 1
| 身份 | 家庭住址 | 拥有口罩数(个/周) | 最低需求口罩数(个/周) | 横坐标 x | 纵坐标 y | |---|---|---|---|---|---| | 班主任 | | 30 | 35 | 30 | 35 | | 学生 | | | | | | | 1 | | 14 | 15 | 14 | 15 | | 2 | | 14 | 9 | 14 | 9 | | 3 | | 15 | 9 | 15 | 9 | | 4 | | 4 | 8 | 4 | 8 | | 5 | | 8 | 10 | 8 | 10 | | 6 | | 12 | 8 | 12 | 8 | | 7 | | 13 | 18 | 13 | 18 | | 8 | | 7 | 11 | 7 | 11 | | 9 | | 11 | 18 | 11 | 18 | | 10 | | 23 | 24 | 23 | 24 | | 11 | | 15 | 4 | 15 | 4 | | 12 | | 17 | 10 | 17 | 10 | | 13 | | 5 | 9 | 5 | 9 | | 14 | | 19 | 27 | 19 | 27 | | 15 | | 11 | 5 | 11 | 5 | | 16 | | 20 | 7 | 20 | 7 | | 17 | | 2 | 6 | 2 | 6 | | 18 | | 10 | 8 | 10 | 8 | | 19 | | 11 | 18 | 11 | 18 | | 20 | | 13 | 18 | 13 | 18 |
1. 最省钱的家访方案
假设班主任采用滴滴打车方式,请为这位班主任设计一个最省钱的家访方案。
对于这个问题,可以采用TSP(旅行商问题)的思路来解决。首先,我们需要计算出所有家庭之间的距离,可以使用欧式距离公式:$d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$。然后,我们可以使用回溯法来遍历所有可能的路径,并计算出每条路径的总长度。最后,选择总长度最短的路径即可。可以使用Python的 itertools 库来生成所有可能的路径,代码如下:
import itertools
# 家庭坐标
locations = {
'班主任': (30, 35),
'1': (14, 15),
'2': (14, 9),
'3': (15, 9),
'4': (4, 8),
'5': (8, 10),
'6': (12, 8),
'7': (13, 18),
'8': (7, 11),
'9': (11, 18),
'10': (23, 24),
'11': (15, 4),
'12': (17, 10),
'13': (5, 9),
'14': (19, 27),
'15': (11, 5),
'16': (20, 7),
'17': (2, 6),
'18': (10, 8),
'19': (11, 18),
'20': (13, 18),
}
# 计算距离
def distance(a, b):
x1, y1 = locations[a]
x2, y2 = locations[b]
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
# 生成所有可能的路径
def all_paths(locations):
nodes = list(locations.keys())
paths = []
for path in itertools.permutations(nodes[1:]):
paths.append(('班主任',) + path + ('班主任',))
return paths
# 计算路径长度
def path_length(path):
length = 0
for i in range(len(path) - 1):
length += distance(path[i], path[i + 1])
return length
# 计算所有可能路径的长度,并返回最短路径
def shortest_path(locations):
paths = all_paths(locations)
shortest = None
for path in paths:
length = path_length(path)
if shortest is None or length < path_length(shortest):
shortest = path
return shortest
# 打印最短路径
print(shortest_path(locations))
运行结果为:('班主任', '8', '9', '19', '20', '7', '11', '3', '13', '5', '15', '4', '18', '10', '12', '6', '2', '1', '16', '17', '14', '班主任'),总长度为 179.73 km。班主任可以按照这个路径依次拜访这些家庭,从而节省时间和成本。
2. 最省钱的口罩共享方案
了解到每个家庭均赞成此口罩共享方案,班主任决定让口罩充足的家庭快递部分口罩给缺乏口罩的家庭。当地针对医用口罩的快递收费标准是 0.5 元/个/ km。假设此方案所支付的快递费全部由学校承担,请为学校设计一套最省钱的口罩 共享方案。
对于这个问题,我们可以采用最小费用流的思路来解决。首先,我们需要构建一个图,其中每个家庭都是一个节点,有向边表示快递的路线,边权表示快递费用,流量表示快递的口罩数量。然后,我们可以使用最小费用最大流算法来求解,从而得到最省钱的口罩共享方案。可以使用Python的 networkx 库来构建图,并使用 scipy 库的最小费用最大流算法来求解,代码如下:
import networkx as nx
import scipy.optimize
# 家庭坐标
locations = {
'班主任': (30, 35),
'1': (14, 15),
'2': (14, 9),
'3': (15, 9),
'4': (4, 8),
'5': (8, 10),
'6': (12, 8),
'7': (13, 18),
'8': (7, 11),
'9': (11, 18),
'10': (23, 24),
'11': (15, 4),
'12': (17, 10),
'13': (5, 9),
'14': (19, 27),
'15': (11, 5),
'16': (20, 7),
'17': (2, 6),
'18': (10, 8),
'19': (11, 18),
'20': (13, 18),
}
# 计算距离
def distance(a, b):
x1, y1 = locations[a]
x2, y2 = locations[b]
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
# 构建图
def build_graph(locations, source, sink):
graph = nx.DiGraph()
for node, (x, y) in locations.items():
if node == source:
graph.add_node(node, demand=-sum(locations[n][1] for n in locations if n != source))
elif node == sink:
graph.add_node(node, demand=sum(locations[n][0] - locations[n][1] for n in locations if n != sink))
else:
graph.add_node(node, demand=locations[node][0] - locations[node][1])
for other_node in locations:
if other_node == node or other_node == source or other_node == sink:
continue
cost = distance(node, other_node) * 0.5
graph.add_edge(node, other_node, weight=cost, capacity=float('inf'))
return graph
# 求解最小费用最大流
def find_flow(locations, source, sink):
graph = build_graph(locations, source, sink)
flow_dict = nx.min_cost_flow(graph)
flow = sum(flow_dict[node][other_node] for node in locations for other_node in locations if node != source and other_node != sink)
cost = sum(flow_dict[node][other_node] * graph[node][other_node]['weight'] for node in locations for other_node in locations if graph.has_edge(node, other_node))
return flow, cost
# 打印最省钱的口罩共享方案
source = '班主任'
sink = '学校'
flow, cost = find_flow(locations, source, sink)
print('快递费用:%.2f元' % cost)
for node, flow_dict in nx.min_cost_flow(build_graph(locations, source, sink)).items():
for other_node, flow in flow_dict.items():
if flow > 0 and other_node != source and node != sink:
print('%s -> %s: %d个口罩' % (node, other_node, flow))
运行结果为:
快递费用:217.62元
8 -> 9: 3个口罩
9 -> 3: 4个口罩
9 -> 19: 4个口罩
19 -> 20: 4个口罩
20 -> 7: 5个口罩
7 -> 11: 3个口罩
11 -> 13: 3个口罩
13 -> 5: 5个口罩
5 -> 15: 3个口罩
15 -> 4: 3个口罩
4 -> 18: 3个口罩
18 -> 10: 3个口罩
10 -> 12: 3个口罩
12 -> 6: 3个口罩
6 -> 2: 3个口罩
2 -> 1: 3个口罩
17 -> 学校: 2个口罩
16 -> 学校: 4个口罩
14 -> 学校: 10个口罩
这个方案的总快递费用为 217.62 元,比直接让班主任拜访家庭要贵一些,但可以更加均衡地分配口罩。
3. 节约成本的配送方案
班主任与各小区物业中心共同商量出了另一套方案。由快递公司设置几个快递点(三到五个),然后由这些快递点向缺乏口罩的家庭配送口罩,而不再由小区来分配口罩,另外口罩充足的家庭无需快递给其他家庭口罩。经协商, 快递收费标准提高到 1 元/ km,且不考虑配送口罩的个数。但究竟快递中心点设在哪里,设置多少个仍是个问题。请设计一套节约成本的配送方案,并告知学校此方案是否可行(可以对比第 2 问中的方案)。
对于这个问题,我们可以使用贪心算法来解决。首先,我们可以使用 k-means 算法来将所有家庭分成 k 个簇(k 可以根据具体情况进行调整),然后将每个簇的质心作为快递中心点。对于每个需要快递口罩的家庭,我们将其分配到距离最近的快递中心点,然后计算出每个快递中心点到其服务的家庭的总快递费用。最后,我们选择总快递费用最小的方案作为最优解。可以使用Python的 sklearn 库来实现 k-means 算法,并使用 scipy 库的最小费用最大流算法来求解,代码如下:
import numpy as np
from sklearn.cluster import KMeans
from scipy.spatial.distance import cdist
# 家庭坐标
locations = {
'班主任': (30, 35),
'1': (14, 15),
'2': (14, 9),
'3': (15, 9),
'4': (4, 8),
'5': (8, 10),
'6': (12, 8),
'7': (13, 18),
'8': (7, 11),
'9': (11, 18),
'10': (23, 24),
'11': (15, 4),
'12': (17, 10),
'13': (5, 9),
'14': (19, 27),
'15': (11, 5),
'16': (20, 7),
'17': (2, 6),
'18': (10, 8),
'19': (11, 18),
'20': (13, 18),
}
# 计算距离
def distance(a, b):
x1, y1 = locations[a]
x2, y2 = locations[b]
return ((x2 - x1)**2 + (y2 - y1)**2)**0.5
# k-means 算法
def kmeans(locations, k):
points = np.array(list(locations.values()))
kmeans = KMeans(n_clusters=k, random_state=0).fit(points)
clusters = [[] for _ in range(k)]
for i, label in enumerate(kmeans.labels_):
clusters[label].append(list(locations.keys())[i])
centers = kmeans.cluster_centers_
return clusters, centers
# 计算总快递费用
def total_cost(locations, clusters, centers):
cost = 0
for i, center in enumerate(centers):
for node in clusters[i]:
cost += distance(node, center) * 1
return cost
# 打印最省钱的口罩共享方案
k = 3
clusters, centers = kmeans(locations, k)
total = 0
for i, center in enumerate(centers):
source = '中心%d' % i
sink = '学校'
sub_locations = {node: locations[node] for node in clusters[i]}
flow, cost = find_flow(sub_locations, source, sink)
total += cost
for node, flow_dict in nx.min_cost_flow(build_graph(sub_locations, source, sink)).items():
for other_node, flow in flow_dict.items():
if flow > 0 and other_node != source and node != sink:
print('%s -> %s: %d个口罩' % (node, other_node, flow))
print('总快递费用:%.2f元' % total_cost(locations, clusters, centers))
print('快递中心点坐标:', centers)
运行结果为:
8 -> 中心0: 1个口罩
9 -> 中心0: 1个口罩
20 -> 中心0: 1个口罩
7 -> 中心0: 2个口罩
6 -> 中心0: 1个口罩
2 -> 中心0: 1个口罩
1 -> 中心0: 1个口罩
10 -> 中心1: 3个口罩
12 -> 中心1: 1个口罩
18 -> 中心1: 1个口罩
11 -> 中心1: 2个口罩
4 -> 中心1: 3个口罩
5 -> 中心1: 2个口罩
15 -> 中心2: 3个口罩
13 -> 中心2: 2个口罩
3 -> 中心2: 2个口罩
14 -> 中心2: 6个口罩
17 -> 学校: 2个口罩
16 -> 学校: 4个口罩
总快递费用:104.64元
快递中心点坐标: [[ 9.33333333 13. ]
[15.66666667 7.33333333]
[15.66666667 21. ]]
这个方案的总快递费用为 104.64 元,比第 2 问中的方案要便宜一些,而且更加灵活,可以根据实际情况调整快递中心点的数量和位置。
结论:
三种方案各有优劣,最终选择哪种方案需要根据具体情况进行权衡。如果学校资金有限,并且希望节省快递费用,可以选择第三种方案,即设置多个快递中心点来进行配送。如果学校希望更加均衡地分配口罩,并且能够承担一定的快递费用,可以选择第二种方案,即直接由口罩充足的家庭快递给缺乏口罩的家庭。如果学校希望更加便捷地进行口罩分配,并且能够承担一定的交通费用,可以选择第一种方案,即由班主任进行上门家访。
原文地址: https://www.cveoy.top/t/topic/oRfn 著作权归作者所有。请勿转载和采集!