一.题目要求
用最小生成树结合所提供的论文解决旅行商问题。
二.思路分析
Step1:用Prim算法生成最小生成树
Step2:根据最小生成树中各边的权重及各点的度,从大到小对最小生成树进行剪枝操作,使各点的度为0或1。这个功能在程序中使用函数jianzhi实现的。
Step3:将剪枝的结果连接成环,这个功能在程序中使用cicle函数实现。需要分为3步:
第一步:连接度为0的孤立点,使各点的度大于0,连接成一个个子图
第二步:处理当度为1的点数大于2的情况,将各子图连接为一条链
第三步:连接剩下的两个度为1的点,成为一个环
三.实验程序
# -*-coding:gbk-*-
from numpy import *
import Graph #Graph邻接矩阵类,使用到的功能:.out_edges提供连接节点表(连接节点,权重)///.vertex_num获得顶点数
import PrioQueue #基于堆结构实现优先队列的类
from Graph import Graph
from PrioQueue import PrioQueue
##############################最小生成树算法
def Prim(graph):
vnum = graph.vertex_num()#顶点数
mst = [None]*vnum #最小生成树边表,每个元素的形式为((i,j)w),初始所有点都不属于U,为None。最小生成树原则上有n-1条边,为实现方便,加入了((0,0,),0),共有n条
cands = PrioQueue([(0, 0, 0)]) # 记录候选最短边,形式为(w, vi, vj),vi到vj的权重为w。元组的大小比较逐个比较各个元素,这里首先比较w
count = 0#记录最小生成树顶点数
# 第一次迭代将设置 mst[0] = ((0, 0), 0),不必专门写
while count < vnum and not cands.is_empty():#顶点数为n或发现网络不连通
w, u, v = cands.dequeue() # 顶端弹出,向下筛选,取当时的最短边的(w,i,j)
if mst[v]: #该顶点已加入最小生成树,边表非空
continue # 邻接顶点v已在mst,跳出本次循环继续,弃掉该边
mst[v] = ((u, v), w) # 记录新的MST边和顶点
count += 1
for vi, w in graph.out_edges(v): # 考虑v的邻接顶点vi,某行连接节点表[(节点,权重)。。。]
if not mst[vi]: # 如果对已知在U中的v,相邻接的vi不在U中,则将这条边列入侯选边
cands.enqueue((w, v, vi))#尾端插入,向上筛选
return mst
##############################剪枝
def jianzhi(tree):#最小生成树tree((i,j),S)
tree.remove(((0, 0), 0))#删去为计算方便使用的第一个顶点到自身的边
#排序后从大到小删去度大于2的边
treesort=sorted(tree,key=lambda S: S[1],reverse=True)#lambda是匿名函数,入口为S,返回S[1],将边按距离从大到小进行排序
print('Prim tree按边距从大到小排序treesort:',treesort)
finddu=dict()
for ((u, v), w) in treesort:#建立最小生成树各点度字典
if u not in finddu:
finddu[u]=1
else:finddu[u]+=1
if v not in finddu:
finddu[v]=1
else:finddu[v]+=1
print('最小生成树的度finddu:',finddu)
remove2index=[]
for ((u, v), w) in treesort:
if finddu[u]>2 or finddu[v]>2:
finddu[u]-=1
finddu[v] -= 1
remove2index.append(((u, v), w))
# print(remove2index)
for i in remove2index:#若提前删去会影响上一个for
treesort.remove(i) # 删除列表指定元素,如果某边关联顶点的度大于2,则删掉该边
return treesort
#################################将剪枝后的非连通图处理为环
def cicle(myjianzhi,mymat,mat2):#剪枝后的非连通图,图对象
def matcomdu(tu):#计算度矩阵
zeromat =[0] * mymat.vertex_num()# 1*n行度矩阵
#zeromat = zeros([mymat.vertex_num(), 1])
for ((u, v), w) in tu: # 建立剪枝后各点度字典
zeromat[u] += 1
zeromat[v] += 1
return (zeromat)
def onenum(dumat):#计算1个个数
num = 0
for i in dumat:
if i == 1:
num += 1
return num
def searchminconnect(a,b,mat2):#将a,b集合中的点各选两个进行最小连接
minW=inf
for i in a:
for j in b:
if ((mat2[i][j]<minW) & (mat2[i][j]!=0)):
x=i
y=j
minW=mat2[i][j]
return (x,y)#(x,y)为集合a,b中的最小连接点
addtu=[myjianzhi[i][:] for i in range(len(myjianzhi))]#不同于剪枝图的全新连接图
dumat=matcomdu(addtu)#树中的度矩阵
print('#################处理第一步:连接度为0的孤立点#################')
while 0 in dumat:
onepoint=[]
anotherpoint =[]
for i in range(len(dumat)):
if dumat[i]<2 :#0或1
anotherpoint.append(i)
onepoint.append(dumat.index(0))
anotherpoint.remove(dumat.index(0))
(a2,b2)=searchminconnect(onepoint,anotherpoint,mat2)
addtu.append(((a2,b2),mat2[a2][b2]))
dumat = matcomdu(addtu)
print('第一步去度为0的点,度矩阵dumat:', dumat)
print('第一步去度为0的点,处理后的图addtu:',addtu)
print('#################处理第二步:当度为1的点数大于2的情况#################')
while ((0 not in dumat) & (onenum(dumat)>2)):
onepoint = []
anotherpoint = []
findzitu=[0] * mymat.vertex_num()#用于生成第一个连通子图
addtu1=[addtu[i][:]for i in range(len(addtu))]#备选边,用于删去连通子图时用过的点
num1=0
deleteindex = [inf]#inf是为了初始化任意赋的值
# for i in range(len(dumat)):
while deleteindex!=[]:#用于findzitu生成第一个连通子图的度矩阵
deleteindex = []
for ((a3, b3), w3) in addtu1:
if num1 == 0:#选第一条边相关的边作为第一个子图
num1 = 1
findzitu[a3] += 1
findzitu[b3] += 1
deleteindex.append(((a3, b3), w3))
else:#第一条边以外的相邻边
if (findzitu[a3] != 0 or findzitu[b3] != 0):#有邻接点
findzitu[a3] += 1
findzitu[b3] += 1
deleteindex.append(((a3, b3), w3))
# print('deleteindex',deleteindex)
# print('addtu1',addtu1)
for i in deleteindex:#删去备选边中本次循环中用过的邻接边
addtu1.remove(i)
print('第二步中找到的一条子图中各点的度',findzitu)
###################################
first1point=True#判断是否为第一个度1点
for i in range(len(findzitu)):
if findzitu[i]==1 & first1point==True:
onepoint.append(i)
first1point =False
if findzitu[i]==1 & first1point!=True:
anotherpoint.append(i)
###########################################
for i in range(len(dumat)):
if dumat[i]==1 & dumat[i]not in onepoint:#其他度为1的点
anotherpoint.append(i)
(a5, b5) = searchminconnect(onepoint, anotherpoint,mat2)
addtu.append(((a5, b5), mat2[a5][b5]))
dumat = matcomdu(addtu)
# print('2,addtu:', addtu)
print('第二步减少度为1的点,度矩阵dumat:', dumat)
print('第二步减少度为1的点,处理后的图addtu:', addtu)
print('#################处理第三步:连接剩下的两个度为1的点#################')
if( (0 not in dumat) & (onenum(dumat) ==2)):
x=inf
for i in range(len(dumat)):
if (dumat[i]==1) & (x==inf):
x=i
if (dumat[i]==1) &(x!=inf) :
y=i
addtu.append(((x, y), mat2[x][y]))
print('处理第三步将两头连成环后的addtu:', addtu)
return addtu
四.主程序
#######################
def main():
mat2=[[0,3,3,5,16,5,12,21,23],#
[3,0,2,2,9,4,7,18,19],
[3,2,0,7,13,9,15,22,24],
[5,2,7,0,7,2,2,12,14],
[16,9,13,7,0,15,9,20,11],#
[5,4,9,2,15,0,4,10,17],
[12,7,15,2,9,4,0,6,8],
[21,18,22,12,20,10,6,0,10],
[23,19,24,14,11,17,8,10,0]]#
mymat = Graph(mat2)#构造图对象
mymst=Prim(mymat)#构造最小生成树
print('the Prim tree is',mymst)
myjianzhi=jianzhi(mymst)#去除度大于2的部分
print('删去度大于2的边后的树:', myjianzhi)
mycicle=cicle(myjianzhi,mymat,mat2)#剪枝后的非连通图,图对象
print('#################旅行商问题的最终解#################')
print('处理后的环',mycicle)
if __name__=='__main__':
main()
五.实验结果
C:\Users\Administrator\AppData\Local\Programs\Python\Python36\python.exe C:/Users/Administrator/PycharmProjects/untitled1/lvxingshang.py
the Prim tree is [((0, 0), 0), ((0, 1), 3), ((1, 2), 2), ((1, 3), 2), ((3, 4), 7), ((3, 5), 2), ((3, 6), 2), ((6, 7), 6), ((6, 8), 8)]
Prim tree按边距从大到小排序treesort: [((6, 8), 8), ((3, 4), 7), ((6, 7), 6), ((0, 1), 3), ((1, 2), 2), ((1, 3), 2), ((3, 5), 2), ((3, 6), 2)]
最小生成树的度finddu: {6: 3, 8: 1, 3: 4, 4: 1, 7: 1, 0: 1, 1: 3, 2: 1, 5: 1}
删去度大于2的边后的树: [((6, 7), 6), ((1, 2), 2), ((3, 5), 2), ((3, 6), 2)]
#################处理第一步:连接度为0的孤立点#################
第一步去度为0的点,度矩阵dumat: [1, 2, 1, 2, 1, 1, 2, 1, 1]
第一步去度为0的点,处理后的图addtu: [((6, 7), 6), ((1, 2), 2), ((3, 5), 2), ((3, 6), 2), ((0, 1), 3), ((4, 8), 11)]
#################处理第二步:当度为1的点数大于2的情况#################
第二步中找到的一条子图中各点的度 [0, 0, 0, 2, 0, 1, 2, 1, 0]
第二步中找到的一条子图中各点的度 [2, 2, 1, 2, 0, 2, 2, 1, 0]
第二步减少度为1的点,度矩阵dumat: [2, 2, 2, 2, 2, 2, 2, 1, 1]
第二步减少度为1的点,处理后的图addtu: [((6, 7), 6), ((1, 2), 2), ((3, 5), 2), ((3, 6), 2), ((0, 1), 3), ((4, 8), 11), ((5, 0), 5), ((2, 4), 13)]
#################处理第三步:连接剩下的两个度为1的点#################
处理第三步将两头连成环后的addtu: [((6, 7), 6), ((1, 2), 2), ((3, 5), 2), ((3, 6), 2), ((0, 1), 3), ((4, 8), 11), ((5, 0), 5), ((2, 4), 13), ((7, 8), 10)]
#################旅行商问题的最终解#################
处理后的环 [((6, 7), 6), ((1, 2), 2), ((3, 5), 2), ((3, 6), 2), ((0, 1), 3), ((4, 8), 11), ((5, 0), 5), ((2, 4), 13), ((7, 8), 10)]
Process finished with exit code 0
六.结果分析
首先对比一下调试结果与论文中的结果。将调试结果转化为连通图如下图所示:
注:由于python中索引是从0开始的,所以程序将各点编号为0,1,2...8对应于论文中的1,2,3...9
论文中的连通图为:
两者之间的连接方式显然不同,但是论文中各边权重之和为58,而调试结果中,各边权重之和为54。由此可见,程序在本例中运行结果是优于参考论文的。有多种可能导致了这种情况的发生:
1. 在对最小生成树剪枝时,需要对prim树中的边权重进行排序,而prim树中有多条权重为2的边,对这些权重为2的边的排序方式的不同会导致剪枝后的结果不同。
2. 在“Step3:将剪枝的结果连接成环”这一步中,考虑到时间复杂性,并不是从整体最优上加以考虑,而是选择算法遇到的第一个度为0/1的点作为第一个需要连接的点,然后从余下的点中运用贪心算法,找到某种意义上的局部最优解。对“第一个需要连接的点”的选择也会导致最后结果的不同。