大学校园又将迎接新生的到来,又到了卖电话卡的好时候的。当然,人又不能分身,怎么挑选合适的点位去卖电话卡,这是个问题......
一、题目
你准备在某大学规划两个电话卡销售摊位,在这个学校7个学院的学生宿舍门口销售校园电话卡。每个学院宿舍的学生数量(单位:十人)如下图所示。每个销售摊位只能兼顾两个学院宿舍的销售,即向所在学院宿舍和一个相邻学院的宿舍学生销售电话卡。问这两个销售摊位应该设在哪两个学院的宿舍内,你才能卖出更多的电话卡?
二、思路解析
1.前导
①模型选取
题目让我们在七个区域里面选两个分别能够辐射两个区域的区来售卖,要选择最优的点位让售量最大化。首先可以判断出这是一个优化类的问题,也就是找一个最优的方案。那么想到优化类的问题,我们就可以想到线性规划、非线性规划和整数规划。根据这个题目的条件,可以大致判断出是整数规划,而且是0-1规划这个分支。
②0-1规划概述
简要的定义, 0-1 规划 (0-1 Programming / Binary Programming)作为整数规划的一个特殊分支,属于决策变量只能取 0 或 1的规划问题。我们一般把1指代选择,0指代不选择,它在需要进行“是/否”决策、选择、分配的题目中比较通用,了解过01背包DP算法的朋友可能就会比较熟悉。
2.模型构建
①模型假设
为了能够去聚焦问题的核心,我们就得通过若干个假设,把模型简化,把相对不重要的因素忽略掉,让我们整个模型计算更加简洁
- 忽略人员的流动性,避免出现换宿舍的情况,即保证每个区域都始终是这么多人
- 供需保证最大化,即有多少人就卖多少卡
- 保证销售点不重叠,即两个设立区域的辐射区域不重叠交叉
②指标建立
我们先给图片中的各个区域给标号:
初步分析一下,可以直接从图中得到一些基本的信息(需要注意的是2和5是不相邻的):
- 1与2、3相邻
- 2与1、3、4相邻
- 3与1、2、4相邻
- 4与2、3、5、6、7相邻
- 5与4、6相邻
- 6与4、5、7相邻
- 7与4、6相邻
因此,我们便可以构建一个无向图:
设:
,其中点
建在区
上时取值为 1,此为决策变量。
是区
权值,即该区学生人数。
为相邻区对集合。
为所有区的集合。
为覆盖区的学生总人数,有可以展开成目标函数:
然后约束条件是:
- 限制代建点数:
即选出两个代建点。
- 限制代建点覆盖区域:
即一个代建点只覆盖一个邻区,不重合。举几个例子:
- 限制代建点覆盖连续区间:
3.解决方法
①MATLAB
将无向图转化为邻接关系,以矩阵存储,原理类似邻接表。
其中节点代表区域,权值为人数,边代表相邻关系。然后伪代码如下:
- 初始化解,输入;
- 调制级数枚举所有边对点,先第一条,再第二条边;
- 减取两条边,去重算覆盖区域并求计算总学生数;
- 遍历选取最优解;
- 输出。
整体思路是搜索,复杂度为 O(), n为边数。
操作的部分代码:
population = [34, 29, 42, 21, 56, 18, 71];% 区域学生数量(十人)
% 定义邻接关系(无向图)
edges = [1 2; % 边1
1 3; % 边2
2 3; % 边3
2 4; % 边4
3 4; % 边5
4 5; % 边6
4 6; % 边7
4 7; % 边8
5 6; % 边9
6 7]; % 边10
num_edges = size(edges, 1);
max_coverage = 0;
best_edges = [];
covered_areas = [];
for i = 1:num_edges
for j = i+1:num_edges
edge1 = edges(i, :);
edge2 = edges(j, :);
% 计算覆盖区域并集
all_areas = unique([edge1, edge2]);
% 计算覆盖学生总数
total_pop = sum(population(all_areas));
if total_pop > max_coverage
max_coverage = total_pop;
best_edges = [i, j];
covered_areas = all_areas;
end
end
end
然后我们就可以直接打印的结果
% 输出
fprintf('最优代理点服务对组合: 边%d (%d-%d) 和 边%d (%d-%d)\n', ...
best_edges(1), edges(best_edges(1),1), edges(best_edges(1),2), ...
best_edges(2), edges(best_edges(2),1), edges(best_edges(2),2));
fprintf('可选择的方案: \n');
fprintf('方案1: 区域%d(覆盖%d和%d) + 区域%d(覆盖%d和%d)\n', ...
edges(best_edges(1),1), edges(best_edges(1),1), edges(best_edges(1),2), ...
edges(best_edges(2),1), edges(best_edges(2),1), edges(best_edges(2),2));
fprintf('方案2: 区域%d(覆盖%d和%d) + 区域%d(覆盖%d和%d)\n', ...
edges(best_edges(1),1), edges(best_edges(1),1), edges(best_edges(1),2), ...
edges(best_edges(2),2), edges(best_edges(2),2), edges(best_edges(2),1));
fprintf('方案3: 区域%d(覆盖%d和%d) + 区域%d(覆盖%d和%d)\n', ...
edges(best_edges(1),2), edges(best_edges(1),2), edges(best_edges(1),1), ...
edges(best_edges(2),1), edges(best_edges(2),1), edges(best_edges(2),2));
fprintf('方案4: 区域%d(覆盖%d和%d) + 区域%d(覆盖%d和%d)\n', ...
edges(best_edges(1),2), edges(best_edges(1),2), edges(best_edges(1),1), ...
edges(best_edges(2),2), edges(best_edges(2),2), edges(best_edges(2),1));
fprintf('\n覆盖区域: %s\n', mat2str(covered_areas));
fprintf('覆盖学生数: %d十人 = %d人\n', max_coverage, max_coverage*10);
如果想要直观的可视化,我们还可以生成可以提现最终选择情况的无向图
figure;
g = graph([1 1 2 2 3 4 4 4 5 6], [2 3 3 4 4 5 6 7 6 7]);
p = plot(g, 'NodeLabel', {'1(340)','2(290)','3(420)','4(210)','5(560)','6(180)','7(710)'});
highlight(p, 1:numnodes(g), 'NodeColor', 'k', 'MarkerSize', 6);
highlight(p, covered_areas, 'NodeColor', 'r', 'MarkerSize', 6);
title(sprintf('最优覆盖方案: 总共%d0学生', max_coverage));
II. Python
代码如下:
import numpy as np
population = np.array([34, 29, 42, 21, 56, 18, 71])
edges = np.array([
[1, 2], # 边1
[1, 3], # 边2
[2, 3], # 边3
[2, 4], # 边4
[3, 4], # 边5
[4, 5], # 边6
[4, 6], # 边7
[4, 7], # 边8
[5, 6], # 边9
[6, 7] # 边10
])
num_edges = edges.shape[0]
max_coverage = 0
best_edge_indices = []
covered_areas = []
for i in range(num_edges):
for j in range(i + 1, num_edges):
edge1 = edges[i]
edge2 = edges[j]
all_areas_set = set(edge1) | set(edge2)
all_areas = sorted(list(all_areas_set))
total_pop = sum(population[area - 1] for area in all_areas)
if total_pop > max_coverage:
max_coverage = total_pop
best_edge_indices = [i, j]
covered_areas = all_areas
# 输出结果
best_edge_1_idx = best_edge_indices[0]
best_edge_2_idx = best_edge_indices[1]
edge1_nodes = edges[best_edge_1_idx]
edge2_nodes = edges[best_edge_2_idx]
print(f"最优代理点服务对组合: 边{best_edge_1_idx + 1} ({edge1_nodes[0]}-{edge1_nodes[1]}) 和 边{best_edge_2_idx + 1} ({edge2_nodes[0]}-{edge2_nodes[1]})")
print("可选择的代理点建设方案: ")
# 方案1
print(f"方案1: 区域{edge1_nodes[0]}(覆盖{edge1_nodes[0]}和{edge1_nodes[1]}) + 区域{edge2_nodes[0]}(覆盖{edge2_nodes[0]}和{edge2_nodes[1]})")
# 方案2
print(f"方案2: 区域{edge1_nodes[0]}(覆盖{edge1_nodes[0]}和{edge1_nodes[1]}) + 区域{edge2_nodes[1]}(覆盖{edge2_nodes[1]}和{edge2_nodes[0]})")
# 方案3
print(f"方案3: 区域{edge1_nodes[1]}(覆盖{edge1_nodes[1]}和{edge1_nodes[0]}) + 区域{edge2_nodes[0]}(覆盖{edge2_nodes[0]}和{edge2_nodes[1]})")
# 方案4
print(f"方案4: 区域{edge1_nodes[1]}(覆盖{edge1_nodes[1]}和{edge1_nodes[0]}) + 区域{edge2_nodes[1]}(覆盖{edge2_nodes[1]}和{edge2_nodes[0]})")
print(f"\n覆盖区域: {covered_areas}")
print(f"覆盖学生数: {max_coverage}十人 = {max_coverage * 10}人")
同样的,也是用的暴力搜索的方法,相较于0-1规划所属的离散优化问题常用的分支定界法,这种就非常直观简单,当然也牺牲了一定的效率与复杂度。
整体的代码思路就是
- 初始化
- 遍历所有可能的两条边的组合
- 对每条选定的边获取相应的节点权值
- 不断更新最优解
- 打印
简明易懂。
比较需要注意的一个点:Python中列表索引从0开始,而前面我们在MATLAB的区域编号是从1开始的,所以很多在访问的时候都需要-1
4.答案结果
构建模型、运行代码之后,我们就可以得到结果:
①MATLAB
最优的选择还有最优解覆盖的人数都被打印出来了:
还有相应的可视化无向图:
我们设置红色是覆盖的,黑色是没有覆盖的,这样子看起来就很直观了。
②Python
答案结果也是一样的:
根据答案,我们简要做一个可视化
import networkx as nx
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # 微软雅黑
plt.rcParams['axes.unicode_minus'] = False # 负号显示
population = [34, 29, 42, 21, 56, 18, 71]
edges = [
(1, 2),
(1, 3),
(2, 3),
(2, 4),
(3, 4),
(4, 5),
(4, 6),
(4, 7),
(5, 6),
(6, 7)
]
# 构建无向图
G = nx.Graph()
G.add_edges_from(edges)
covered_areas = [1, 3, 4, 7]
node_colors = ['red' if node in covered_areas else 'black' for node in G.nodes()]
# 绘图
pos = nx.spring_layout(G, seed=42) #
labels = {i: f"{i}({population[i-1]*10})" for i in G.nodes()}
label_pos = {node: (x + 0.10, y) for node, (x, y) in pos.items()}
plt.figure(figsize=(10, 8))
# 节点
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=800)
# 绘制边
nx.draw_networkx_edges(G, pos, width=1.5, alpha=0.7, edge_color='gray')
node_numbers = {i: str(i) for i in G.nodes()}
nx.draw_networkx_labels(G, pos, labels=node_numbers, font_size=12, font_color='white')
nx.draw_networkx_labels(G, label_pos, labels=labels, font_size=10, font_color='black')
plt.title('最优覆盖方案: 红为覆盖节点,黑为未覆盖节点', fontsize=16) # 增大标题字体
plt.axis('off')
plt.show()
也是十分清晰的:
最终我们可以得到答案,即[1,4,3,7]的组合,展开来说就是
- 选择 1 区,3 区,覆盖 4 区,7 区
- 选择 4 区,3 区,覆盖 1 区,7 区
- 选择 1 区,7 区,覆盖 4 区,3 区
- 选择 4 区,7 区,覆盖 1 区,3 区
四种组合相应能卖卡给到的人数都为1680人。
THE END