图与网络——中国邮递员问题的Pyhton实现
中国邮递员问题是邮递员在某一地区的信件投递路程问题。邮递员每天从邮局出发,走遍该地区所有街道再返回邮局,问题是他应如何安排送信的路线可以使所走的总路程最短。这个问题由中国学者管梅谷在1960年首先提出,并给出了解法——“奇偶点图上作业法”,被国际上统称为“中国邮递员问题”。用图论的语言描述,给定一个连通图G,每边e有非负权),要求一条回路经过每条边至少一次,且满足总权最小。比如:扫雪车、处理垃圾车、散水车、送信员等路线规划。
中国邮递员问题算法流程图 | Fleury算法流程 |
---|---|
一、中国邮递员问题
一个邮递员从邮局出发,要走完他所管辖范围内的每一条街道至少一次再返回邮局,如何选择一条尽可能短的路线?这就是中国邮递员问题(Chinese Postman Problem),简称CPP问题。
1.1 中国邮递员问题的数学模型
对于图\(G_1\left(V, E_1\right)\),每个 \(E_1\) 唯一对应一组 \(x_{ij}\)变量,反之亦然。可以借助变量 \(x_{ij}(\mathrm{i}=1,2, \cdots, n ; j=1,2, \cdots, n)\) 来定义邮递员问题,约束如下:
(1) 过每边至少 1 次且添加边至多 1 条,\(E_1\) 对应的所有的 \(x_{ij}\) 的值 (称为\(E_1\) 的值系) 满足: 对 \(\forall e_{ij} \in E_1, x_{ij}+x_{ji} \geq 1\)
(2) 图 \(G_1\left(V, E_1\right)\) 不含奇点, 即对于任意一个顶点 \(v_i\), 有进向弧, 必有与之等量的出向弧: \(\sum_{j \neq i} x_{ji}-\sum_{j \neq i} x_{ij}=0\) 这一问题的目标是使得 \(G_1\left(V, E_1\right)\) 的总权最小, 即 \(\min \sum_{(i,j) \in E_1} w_{ij} x_{ij}\),其中 \(w_{ij}\) 为边 \(e_{ij}\) 上的权,\(w_{ji}=w_{ij}\) 。
这样, 就得到中国邮递员问题的整数规划模型 (CPP)
这一模型不仅可以用于求解中国邮递员问题, 而且可以确定相应的最优投递路线: 如 \(x_{ij}=1\), 即表示邮递员应该从\(v_i\)沿着边 \(e_{ij}\)(即街道) 到 \(v_j\) 。
1.2 欧拉圈
欧拉图:给定一个连通无向图\(G\),则称经过\(G\)的每条边一次并且仅一次的路径为欧拉通路;如果欧拉通路是回路(起点和终点是同一个顶点),则称此回路为欧拉回路(Euler circuit);具有欧拉回路的无向图G称为欧拉图(Euler graph)。
中国邮递员问题可用图论语言叙述为:在一个具有非负权的带权连通图\(G\)中,找出一条总权重最小的环游,这种环游称为最优环游。
① \(G\)是欧拉图,则\(G\)的任意欧拉回路都是最优环游。
②\(G\)不是欧拉图,则\(G\)的任意一个环游必定通过某些边不止一次。将边\(e\)的两个端点再用一条权为\(w(e)\)的新边连接时,称边\(e\)为重复的。此时CPP问题与下述问题等价:
若\(G\)是给定的有非赋权的赋权连通图,用添加重复边的方法求欧拉回路\(G\)的一个欧拉赋权母图\(G^*\),满足
求\(G^*\)的欧拉回路。
二、CPP解法
2.1 奇偶点图上作业法
1960年我国管梅谷发表于数学学报上的论文“奇偶点图上作业法”,是针对于中国邮递员问题的最早论文,将关于“一笔画”问题的一些已知结果与物资调拨中的图上作业法的基本思想相结合,得到了CPP问题中添边策略的一种方法。
奇偶点:根据顶点连接边的次数划分。
①生成初始可行方案:
若图中有奇点,则把它配成对,每一对奇点之间必有一条链,把这条链的所有边作为重复边加到图中去,新图中必无奇点。便给出了第一个可行方案。
②调整可行方案:
使重复边总长度下降.当边\((w,v)\)上有两条或两条以上的重复边时,从中去掉偶数条,得到一个总长度较小的方案。于是有:
1)在最优方案中,图的每条边上最多有一条重复边。
2)在最优方案中,图中每个圈上的重复边的总权不大于该圈总权的一半。
③判断最优方案的标准:
一个最优方案一定是满足上述1)和2)的可行方案。反之,一个可行方案若满足上述1)和2),则这个可行方案一定是最优方案。
根据判断标准,对给定的可行方案,检查它是否满足上述条件1)和2)。若满足,所得方案即为最优方案;若不满足,则对方案进行调整,直至上述条件1)和2)均得到满足时为止。
2.2 最小二分匹配法
奇偶点图上作业法是全球范围内研究CPP问题的先驱,他提出了一种添边策略,其中最关键的是指出了一非欧拉图向欧拉图转化的实质是奇点之间的两两匹配,联想到图论二分匹配中的最大权匹配,可用最小权匹配法来完成非欧拉图向欧拉图的转换。
设\(G=(V,E)\)为一简单无向连通图, \(D\)为使用floyd算法求得的图的最短路程矩阵,\(P\)为对应的路径矩阵。\(V_1\)为图\(G\)的奇点集,由图论基础知识可证明一简单图的奇点个数为偶数,记其为\(n\)。构建二分图 \(B=(S,T,E^{'})\),其中 \(S = T =V_1\), \(E^{'}\)的构建如下:
求该二分图的最小权匹配,引入决策变量 \(x_{i j}=0,1\) 来表示 \(S_i\) 与 \(T_j\) 的匹配关系,若 \(x_{i j}=1\) 则表示与 匹配,反之则不匹配,可建立数学规划模型如下:
由该模型求出奇点集的两两匹配,再结合floyd算法得到的最短路程矩阵对应的路径矩阵,可得到 \(G\) 由生成的最小权欧拉图 \(G^*=\left(V, E^*\right)\) 。则此时CPP问题的最优目标值已可求出,即是将的边集中的 每一条边都走一遍,其值为
2.3 fleury算法
无论是无向图还是有向图,皆通过上述模型求解奇点的最小权匹配并由此构造出对应于原图的最小权欧拉图,由中国邮递员问题中的图论语言描述中推导出的等价问题可知,原问题已转化为求欧拉图的一条欧拉回路。
故这里对如何求欧拉图中的欧拉回路展开分析,fleury算法是一种常用的求欧拉图中一条欧拉回路的算法。
设\(G=(V,E)\)为一欧拉图,下为fleury 算法的算法流程:
STEP1 任取$v_0\in V $ ,令 \(C_0 =v_0\) ;
STEP2 假设当前已沿迹$C_i =v_0e_1v_1e_2v_2 \cdots e_iv_i $来到顶点 \(v_i\),按照如下规则从边集 \(E-\{e_1,e_2,\cdots,e_i\}\)中选取\(e_{i+1}\):
⑴ \(e_{i+1}\)与 \(v_i\)关联;
⑵ 除非无其他可选边,否则\(e_{i+1}\)不为图\(G_i=G-\{e_1,e_2,\cdots ,e_i\}\)的割边。
STEP3 当 STEP2 无法继续进行时停止算法。
当算法停止时,得到的迹$ C_m =v_0e_1v_1e_2v_2 \cdots e_mv_m $为图 \(G\)的一条欧拉回路。
定理: 由fleury 算法求得的迹必为欧拉回路。
三、Python计算程序
案例1:现在一个邮递员从邮局出发,要走完他所管辖范围内的每一条街道至少一次再返回邮局,如何选择一条尽可能短的路线,现假设该城市的区域,道路网络如下图所示,线代表道路,点代表十字路口。各条路的成本为(i, j, d),距离在图中已经标出(单位KM),现在请问:送货员从“1”点出发,如何选择最短路线,每条街道至少经过一次,送完邮件后返回“1” 点。
输入数据(边列表)结构如下:
node1 | node2 | trail | distance |
---|---|---|---|
1 | 3 | a | 5 |
1 | 2 | b | 6 |
2 | 5 | c | 1 |
2 | 6 | d | 4 |
3 | 7 | e | 2 |
... | ... | ... | ... |
from postman_problems.solver import cpp
from postman_problems.stats import calculate_postman_solution_stats
# find CPP solution
circuit, graph = cpp(edgelist_filename='edge2.csv', start_node='1')
# print solution route
for e in circuit:
print(e)
# print solution summary stats
for k, v in calculate_postman_solution_stats(circuit).items():
print(k, v)
('1', '2', 0, {'trail': 'b', 'distance': 6, 'id': 1})
('2', '6', 0, {'trail': 'd', 'distance': 4, 'id': 3, 'augmented': True})
('6', '4', 0, {'trail': 'g', 'distance': 1, 'id': 6})
('4', '11', 0, {'trail': 'i', 'distance': 2, 'id': 8, 'augmented': True})
('11', '9', 0, {'trail': 'o', 'distance': 6, 'id': 14})
('9', '12', 0, {'trail': 'n', 'distance': 4, 'id': 13, 'augmented': True})
('12', '9', 0, {'trail': 'n', 'distance': 4, 'id': 13})
('9', '7', 0, {'trail': 'k', 'distance': 2, 'id': 10})
('7', '10', 0, {'trail': 'j', 'distance': 3, 'id': 9})
('10', '12', 0, {'trail': 'p', 'distance': 2, 'id': 15})
('12', '8', 0, {'trail': 'm', 'distance': 7, 'id': 12})
('8', '11', 0, {'trail': 'l', 'distance': 3, 'id': 11})
('11', '4', 0, {'trail': 'i', 'distance': 2, 'id': 8})
('4', '5', 0, {'trail': 'h', 'distance': 3, 'id': 7})
('5', '2', 0, {'trail': 'c', 'distance': 1, 'id': 2})
('2', '6', 0, {'trail': 'd', 'distance': 4, 'id': 3})
('6', '3', 0, {'trail': 'f', 'distance': 1, 'id': 5})
('3', '7', 0, {'trail': 'e', 'distance': 2, 'id': 4, 'augmented': True})
('7', '3', 0, {'trail': 'e', 'distance': 2, 'id': 4})
('3', '1', 0, {'trail': 'a', 'distance': 5, 'id': 0})
distance_walked 64 #邮递员行走路长为64
案例2:点F是邮局所在地点,该邮局由邮递员进行派件,尝试给出合理的派件路线(每条道路需来回各送一次)。这样的图必为欧拉图,所以我们无需考虑生成欧拉图的部分,直接对图进行划分。
输入数据(边列表)结构如下:
node1 | node2 | trail | distance |
---|---|---|---|
A | B | a | 4 |
A | D | b | 3 |
B | C | c | 1 |
... | ... | ... | ... |
from postman_problems.solver import cpp
from postman_problems.stats import calculate_postman_solution_stats
# find CPP solution
circuit, graph = cpp(edgelist_filename='edge4.csv', start_node='F') #邮局所在点是F
# print solution route
for e in circuit:
print(e)
# print solution summary stats
for k, v in calculate_postman_solution_stats(circuit).items():
print(k, v)
#邮递员路线长度为52,路线如下:
('F', 'I', 0, {'trail': 'i', 'distance': 2, 'id': 8, 'augmented': True})
('I', 'L', 0, {'trail': 'n', 'distance': 3, 'id': 13})
('L', 'K', 0, {'trail': 'p', 'distance': 3, 'id': 15})
('K', 'H', 0, {'trail': 'm', 'distance': 3, 'id': 12, 'augmented': True})
('H', 'K', 0, {'trail': 'm', 'distance': 3, 'id': 12})
('K', 'J', 0, {'trail': 'o', 'distance': 2, 'id': 14})
('J', 'G', 0, {'trail': 'k', 'distance': 3, 'id': 10})
('G', 'D', 0, {'trail': 'g', 'distance': 2, 'id': 6, 'augmented': True})
('D', 'G', 0, {'trail': 'g', 'distance': 2, 'id': 6})
('G', 'H', 0, {'trail': 'j', 'distance': 2, 'id': 9})
('H', 'I', 0, {'trail': 'l', 'distance': 3, 'id': 11})
('I', 'F', 0, {'trail': 'i', 'distance': 2, 'id': 8})
('F', 'E', 0, {'trail': 'h', 'distance': 1, 'id': 7})
('E', 'B', 0, {'trail': 'd', 'distance': 3, 'id': 3, 'augmented': True})
('B', 'E', 0, {'trail': 'd', 'distance': 3, 'id': 3})
('E', 'D', 0, {'trail': 'f', 'distance': 4, 'id': 5})
('D', 'A', 0, {'trail': 'b', 'distance': 3, 'id': 1})
('A', 'B', 0, {'trail': 'a', 'distance': 4, 'id': 0})
('B', 'C', 0, {'trail': 'c', 'distance': 1, 'id': 2})
('C', 'F', 0, {'trail': 'e', 'distance': 3, 'id': 4})
总结
中国邮路问题(Chinese Postman Problem)是一个非常经典的图论问题:一个邮递员送信,要走完他负责投递的全部街道(所有街道都是双向通行的且每条街道可以经过不止一次),完成任务后回到邮局,应按怎样的路线走,他所走的路程才会最短呢?如果将这个问题抽象成图论的语言,就是给定一个连通图,每条边的权值就是街道的长度,本问题转化为在图中求一条回路,使得回路的总权值最小。
如果街道的连通图为欧拉图,则只要求出图中的一条欧拉回路即可。否则,邮递员要完成任务就必须在某些街道上重复走若干次。如果重复走一次,就加一条平行边,于是原来对应的图形就变成了多重图。只是要求加进的平行边的总权值最小就行了。于是,我们的问题就转化为,在一个有奇度数结点的赋权连通图中,增加一些平行边,使得新图不含奇度数结点,并且增加的边的总权值最小。求所增加边总权值最小的方案,需要我们找出所有奇度顶点(偶数个)来两两分组,对每小组中的两个点求其最短路径,进而求出每分组的总权值。对所有分组情况,找出最小权值即是最佳方案。