39、TSP的几种精确求解方法(DFJ、MTZ、最短边破圈式、行生成式)和启发式方法(插入法、近邻法、2-opt、3-opt、模拟退火)

本文主要是介绍39、TSP的几种精确求解方法(DFJ、MTZ、最短边破圈式、行生成式)和启发式方法(插入法、近邻法、2-opt、3-opt、模拟退火),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

0、定义

TSP(Traveling Salesman Problem),旅行商问题,又名旅行推销员问题、货郎担问题

假设一个旅行商要拜访n个城市,每个城市只能经过一次,且最后要回到起点,问如何走路程最短

1、整数模型

设cij为第i个点到第j个点的距离,xij表示是否从走到

1.1、DFJ整数模型

该模型较好理解,前面两个约束保证对每个点入度为1,出度也为1,第三个约束保证每个非空集合到其补集一定有边相连

def TSP_IP_DFJ(Dis, n, time_limit):model = Model("TSP_IP_DFJ")A = [[model.addVar(vtype="B", name="A[%s,%s]" % (i, j)) for j in range(n)] for i in range(n)]# 以总成本最小为目标model.setObjective(quicksum(Dis[i][j] * A[i][j] for i in range(n) for j in range(n)))#一度约束for i in range(n):model.addCons(quicksum(A[i][j] for j in range(n) if i != j) == 1)model.addCons(quicksum(A[j][i] for j in range(n) if i != j) == 1)#无子环约束SS = subset([i for i in range(n)])for S in SS:model.addCons(quicksum(A[i][j] for i in S for j in range(n) if j not in S) >= 1)# 设置求解时间model.setRealParam("limits/time", time_limit)model.optimize()print("\ngap:", model.getGap())A1 = [[round(model.getVal(A[i][j])) for j in range(n)] for i in range(n)]return A1

1.2、MTZ整数模型

两个模型区别在于无子环约束形式的不同,下面证明两者等价

证明

MTZ模型的好处在于其约束相较于DFJ明显变少(2^n到n^2)

def TSP_IP_MTZ(Dis, n, time_limit):model = Model("TSP_IP_MTZ")A = [[model.addVar(vtype="B", name="A[%s,%s]" % (i, j)) for j in range(n)] for i in range(n)]U = [model.addVar(name="U[%s]"%i) for i in range(n)]# 以总成本最小为目标model.setObjective(quicksum(Dis[i][j] * A[i][j] for i in range(n) for j in range(n)))# 一度约束for i in range(n):model.addCons(quicksum(A[i][j] for j in range(n) if i != j) == 1)model.addCons(quicksum(A[j][i] for j in range(n) if i != j) == 1)#MTZ无子环约束for i in range(1, n):for j in range(1, n):if i != j:model.addCons(U[i]-U[j]+(n-1)*A[i][j] <= n-2)# 设置求解时间model.setRealParam("limits/time", time_limit)model.optimize()print("\ngap:", model.getGap())A1 = [[round(model.getVal(A[i][j])) for j in range(n)] for i in range(n)]return A1

1.3、行生成模型

这里先解上面其松弛模型,再判定解是否满足条件,再不断添加不满足的约束,添加约束的方案一般为约束已生成的子环不会发生,下面列举一些方法

让其与补集有边相连,或让其内部连边不够,代码块里通过type区分(精确解)

1.3.1、多次调用模式

  • def sub_TSP_row_generate(Dis, n, L_S, time_limit, type=2):model = Model("sub_TSP_row_generate")A = [[model.addVar(vtype="B", name="A[%s,%s]" % (i, j)) for j in range(n)] for i in range(n)]# 以总成本最小为目标model.setObjective(quicksum(Dis[i][j] * A[i][j] for i in range(n) for j in range(n)))# 一度约束for i in range(n):model.addCons(quicksum(A[i][j] for j in range(n) if i != j) == 1)model.addCons(quicksum(A[j][i] for j in range(n) if i != j) == 1)#两个方向限制不生成已有的子环if type == 1:#该集合与补集有边相连for S in L_S:model.addCons(quicksum(A[i][j] for i in S for j in range(n) if j not in S) >= 1)model.addCons(quicksum(A[j][i] for i in S for j in range(n) if j not in S) >= 1)else:#该集合内部边不够for S in L_S:model.addCons(quicksum(A[i][j] for i in S for j in S if i != j) <= len(S)-1)# 设置求解时间model.setRealParam("limits/time", time_limit)model.optimize()print("\ngap:", model.getGap())A1 = [[round(model.getVal(A[i][j])) for j in range(n)] for i in range(n)]return A1def TSP_IP_row_generate(Dis, n, time_limit):L_S = []ring_sub = Truewhile ring_sub:ring_sub = FalseA = sub_TSP_row_generate(Dis, n, L_S, time_limit)D = {i:j for i in range(n) for j in range(n) if A[i][j] == 1}S = {0}a = 0while len(S) < n:b = D[a]if b in S:L_S.append(S)ring_sub = Truebreakelse:S.add(b)a = bif not ring_sub:return A

1.3.2、freeTransform模式

  • def LL_A_generate(A):LL_A = [[i] for i in range(len(A))]D = {i: i for i in range(len(A))}for i in range(len(A)):for j in range(len(A[i])):if A[i][j] == 1 and D[i] != D[j]:index_min = min(D[i], D[j])index_max = max(D[i], D[j])LL_A[index_min] = LL_A[index_min] + LL_A[index_max]del LL_A[index_max]for k in D:if D[k] > index_max:D[k] -= 1for k in LL_A[index_min]:D[k] = index_minreturn LL_Adef TSP_IP_exact(points, Dis, time_limit=1000, type=2):n = len(Dis)model = Model("TSP_IP_exact")model.hideOutput()A = [[model.addVar(ub=1, name="A[%s,%s]" % (i, j)) for j in range(n)] for i in range(n)]# 以总成本最小为目标model.setObjective(quicksum(Dis[i][j] * A[i][j] for i in range(n) for j in range(n) if i != j), "minimize")for i in range(n):for j in range(n):if i == j:model.addCons(A[i][j] == 0)else:model.addCons(A[i][j] <= 1)model.addCons(A[i][j] >= 0)for i in range(n):model.addCons(quicksum(A[i][j] for j in range(n) if i != j) == 1)model.addCons(quicksum(A[j][i] for j in range(n) if i != j) == 1)# 设置求解时间model.setRealParam("limits/time", time_limit)EPS = 1.e-6loop = 0isMIP = Falsewhile True:loop += 1print("精确求解第", loop, "次迭代")model.optimize()if not isMIP:A1 = [[1 if model.getVal(A[i][j]) > EPS else 0 for j in range(n)] for i in range(n)]else:A1 = [[round(model.getVal(A[i][j])) for j in range(n)] for i in range(n)]LL_A = LL_A_generate(A1)if loop % 10 == 0:A2 = [[A1[i][j] if i < j else 0 for j in range(n)] for i in range(n)]A_plot(points, A2, x_min, y_min, x_max, y_max)if len(LL_A) == 1:if not isMIP:print("转为MIP")model.freeTransform()for i in range(len(A)):for j in range(len(A[i])):model.chgVarType(A[i][j], "B")isMIP = Truecontinueelse:breakmodel.freeTransform()# 已测试过的不能在一起的点,避免再次得到if type == 1:for S in LL_A:model.addCons(quicksum(A[i][j] for i in S for j in range(n) if j not in S) >= 1)else:for S in LL_A:model.addCons(quicksum(A[i][j] for i in S for j in S if i != j) <= len(S) - 1)return A1

1.3.3、freeTransform和networkx模式

  • def solve_tsp(V, c, type=1):def addcut(cut_edges):G = networkx.Graph()G.add_edges_from(cut_edges)Components = list(networkx.connected_components(G))if len(Components) == 1:return Falsemodel.freeTransform()if type == 1:# 该集合与补集有边相连for S in Components:model.addCons(quicksum(x[i, j] for i in S for j in V if j not in S) >= 1)print("cut: %s 与其补集相连" % (S))else:# 该集合内部边不够for S in Components:model.addCons(quicksum(x[i, j] for i in S for j in S if j != i) <= len(S) - 1)print("cut: len(%s) <= %s" % (S, len(S) - 1))return Truedef addcut2(cut_edges):G = networkx.Graph()G.add_edges_from(cut_edges)Components = list(networkx.connected_components(G))if len(Components) == 1:return Falsemodel.freeTransform()for S in Components:T = set(V) - set(S)print("S:", S)print("T:", T)model.addCons(quicksum(x[i, j] for i in S for j in T if j > i) +quicksum(x[i, j] for i in T for j in S if j > i) >= 2)print("cut: %s >= 2" % "+".join([("x[%s,%s]" % (i, j)) for i in S for j in T if j > i]))return Truemodel = Model("tsp")x = {}for i in V:for j in V:x[i, j] = model.addVar(ub=1, name="x(%s,%s)" % (i, j))for i in V:model.addCons(quicksum(x[i, j] for j in V if j != i) == 1, "OutDegree(%s)" % i)model.addCons(quicksum(x[j, i] for j in V if j != i) == 1, "InDegree(%s)" % i)model.setObjective(quicksum(c[i, j] * x[i, j] for i in V for j in V if j != i), "minimize")EPS = 1.e-6isMIP = Falsewhile True:model.optimize()edges = []for (i, j) in x:if model.getVal(x[i, j]) > EPS:edges.append((i, j))if addcut(edges) == False:if isMIP:  # integer variables, components connected: solution foundbreakmodel.freeTransform()for (i, j) in x:  # all components connected, switch to integer modelmodel.chgVarType(x[i, j], "B")isMIP = Truereturn model.getObjVal(), edgesdef TSP_IP_exact_official(points, Dis):nums = len(points)V = range(1, nums + 1)c = {(i, j): Dis[i - 1][j - 1] for i in range(1, nums + 1) for j in range(1, nums + 1)}obj, edges = solve_tsp(V, c)A = [[0 for j in range(nums)] for i in range(nums)]for edge in edges:A[edge[0] - 1][edge[1] - 1] = 1return A

1.3.4、最短的边破除该子环(启发式,效果一般)

  • def sub_TSP_row_generate_heuristic(Dis, n, pairs_must, time_limit):model = Model("sub_TSP_row_generate_heuristic")A = [[model.addVar(vtype="B", name="A[%s,%s]" % (i, j)) for j in range(n)] for i in range(n)]# 以总成本最小为目标model.setObjective(quicksum(Dis[i][j] * A[i][j] for i in range(n) for j in range(n)))# 一度约束for i in range(n):model.addCons(quicksum(A[i][j] for j in range(n) if i != j) == 1)model.addCons(quicksum(A[j][i] for j in range(n) if i != j) == 1)#把要连的边连上for pair in pairs_must:i, j = pair[0], pair[1]model.addCons(A[i][j]+A[j][i] == 1)# 设置求解时间model.setRealParam("limits/time", time_limit)model.optimize()print("\ngap:", model.getGap())A1 = [[round(model.getVal(A[i][j])) for j in range(n)] for i in range(n)]return A1def TSP_IP_row_generate_heuristic(points, Dis, n, time_limit):pairs = pair_points_generate(Dis)pairs_must = set()ring_sub = Trueused = {i:0 for i in range(n)}while ring_sub:ring_sub = FalseA = sub_TSP_row_generate_heuristic(Dis, n, pairs_must, time_limit)D = {i:j for i in range(n) for j in range(n) if A[i][j] == 1}S = {0}a = 0while len(S) < n:b = D[a]#若生成子环,则将最短破圈边加入if b in S:ring_sub = Truefor pair in pairs:i, j = pair[0], pair[1]if ((i in S and j not in S) or (i not in S and j in S)) and used[i] <= 1 and used[j] <= 1:pairs_must.add((i, j))used[i] += 1used[j] += 1breakbreakelse:S.add(b)a = bA = deal_overlap(points, A)A = Undirected_to_directed(A)return A

2、近似算法

2.1、插入法

该方法很直观,从一个点开始,不断的把最近的邻居加进来

#近邻插入
def TSP_nearest(points, Dis):n = len(Dis)S = [0 for i in range(len(Dis))]T = [1 for i in range(len(Dis))]# 加入第一个点route = [0]S[0] = 1T[0] = 0# 开始加点while sum(T) > 0:u = route[-1]nexts = [i for i in range(n) if S[i] == 0]v = nexts[0]for j in range(1, len(nexts)):if Dis[u][v] > Dis[u][nexts[j]]:v = nexts[j]route.append(v)S[v] = 1T[v] = 0A = [[0 for j in range(n)] for i in range(n)]for i in range(len(route) - 1):A[route[i]][route[i + 1]] = 1A[route[i + 1]][route[i]] = 1A[route[0]][route[-1]] = 1A[route[-1]][route[0]] = 1A = deal_overlap(points, A)return A

2.2、凸包插入法

先把所有点的凸包算出来,然后把剩余点以损失最小的方式插入路径中

#先着凸包,再插入
def TSP_insert(points, Dis):# 每个坐标对应的索引D = {}for p in points:D[p[0]] = p[1:]ps = list(D.values())b = boundary_find(ps)route = []for p in b:idex = list(D.keys())[list(D.values()).index(p)]if idex not in route:route.append(idex)S = [i for i in range(len(points)) if i not in route]while len(S) > 0:costs = [[Dis[route[-1]][S[i]] + Dis[S[i]][route[0]] - Dis[route[-1]][route[0]]] + [Dis[route[j]][S[i]] + Dis[S[i]][route[j + 1]] - Dis[route[j]][route[j + 1]] for j in range(len(route) - 1)]for i in range(len(S))]new, idex = 0, 0for i in range(len(S)):for j in range(len(route)):if costs[i][j] < costs[new][idex]:new, idex = i, jroute.insert(idex, S[new])del S[new]n = len(points)A = [[0 for j in range(n)] for i in range(n)]for i in range(len(route) - 1):A[route[i]][route[i + 1]] = 1A[route[i + 1]][route[i]] = 1A[route[0]][route[-1]] = 1A[route[-1]][route[0]] = 1A = deal_overlap(points, A)return A

2.3、2-opt

随机初始化,不断的交换两个点的位置,更短则更新

#2-opt
def TSP_2_opt(points, Dis):route = [i for i in range(len(points))]n = len(points)flag = 1while flag == 1:flag = 0for i in range(n - 1):for j in range(i + 1, n):route1 = copy.deepcopy(route)route1[i], route1[j] = route1[j], route1[i]if sub_k_opt(route, Dis) > sub_k_opt(route1, Dis):route = route1flag = 1breakif flag == 1:breakA = [[0 for j in range(n)] for i in range(n)]for i in range(len(route) - 1):A[route[i]][route[i + 1]] = 1A[route[i + 1]][route[i]] = 1A[route[0]][route[-1]] = 1A[route[-1]][route[0]] = 1A = deal_overlap(points, A)return A

2.4、3-opt

在2-opt的基础上,多加入一个换的点

def TSP_3_opt(points, Dis):route = [i for i in range(len(points))]n = len(points)flag = 1while flag == 1:flag = 0for i in range(n - 2):for j in range(i + 1, n - 1):for k in range(j + 1, n):route1, route2, route3, route4, route5 = copy.deepcopy(route), copy.deepcopy(route), copy.deepcopy(route), copy.deepcopy(route), copy.deepcopy(route)route1[i], route1[j], route1[k] = route1[i], route1[k], route1[j]route2[i], route2[j], route2[k] = route2[j], route2[i], route2[k]route3[i], route3[j], route3[k] = route3[j], route3[k], route3[i]route4[i], route4[j], route4[k] = route4[k], route4[i], route4[j]route5[i], route5[j], route5[k] = route5[k], route5[j], route5[i]if sub_k_opt(route, Dis) > sub_k_opt(route1, Dis):route = route1flag = 1if sub_k_opt(route, Dis) > sub_k_opt(route2, Dis):route = route2flag = 1if sub_k_opt(route, Dis) > sub_k_opt(route3, Dis):route = route3flag = 1if sub_k_opt(route, Dis) > sub_k_opt(route4, Dis):route = route4flag = 1if sub_k_opt(route, Dis) > sub_k_opt(route5, Dis):route = route5flag = 1if flag == 1:breakif flag == 1:breakif flag == 1:breakA = [[0 for j in range(n)] for i in range(n)]for i in range(len(route) - 1):A[route[i]][route[i + 1]] = 1A[route[i + 1]][route[i]] = 1A[route[0]][route[-1]] = 1A[route[-1]][route[0]] = 1A = deal_overlap(points, A)return A

2.5、SA

引入模拟退火思想,一定程度接受质量不高的解

def TSP_SA(points, Dis):route = [i for i in range(len(points))]n = len(points)T = 1000 * sub_k_opt(route, Dis)T0 = 1alpha = 0.99flag = 0limit = n * math.log(n)loop = 0while flag < limit or T <= T0:loop += 1if loop % 10 == 0:print("模拟退火进行至:", loop)i = int(rd.random() * (n - 1))j = int(rd.random() * (n - i - 1)) + i + 1route1 = copy.deepcopy(route)route1[i], route1[j] = route1[j], route1[i]if sub_k_opt(route, Dis) > sub_k_opt(route1, Dis):route = route1T = T * alphaflag = 0else:dc = sub_k_opt(route1, Dis) - sub_k_opt(route, Dis)if math.exp(-dc / T) > rd.random():route = route1T = T * alphaflag = 0else:flag += 1A = [[0 for j in range(n)] for i in range(n)]for i in range(len(route) - 1):A[route[i]][route[i + 1]] = 1A[route[i + 1]][route[i]] = 1A[route[0]][route[-1]] = 1A[route[-1]][route[0]] = 1A = deal_overlap(points, A)return A

3、测试结果

3.1、各方法适用范围

方法

类型

适用规模

DFJ

精确解

0-15

MTZ

精确解

0-80

row_generate

精确解

0-20

row_generate_heuristic

近似解

0-100

exact

精确解

0-150

exact_official

精确解

0-150

nearest

近似解

0-

insert

近似解

0-

2-opt

近似解

0-

3-opt

近似解

0-

SA

近似解

0-

3.2、测试结果

这里采用公共数据集中有最优解的case测试,总体来说凸包插入法在求解效率(秒级别出结果)和求解精度(gap4.43%)上表现较好

测试集

最优解

近邻插入

gap

时间

凸包插入

gap

时间

avg8.44%3.344.43%3.29
gr9654490.0658973.968.23%0.3557667.755.83%0.26
rd1007910.409100.2615.04%0.278430.036.57%0.30
pcb44250783.5558982.0416.14%9.5456156.0110.58%10.91
lin10514383.0018027.8925.34%0.3714632.231.73%0.33
tsp2253859.004275.9310.80%2.024377.0713.42%1.94
gr666321086.46338140.025.31%53.76345173.007.50%50.96
ch1306110.866791.3111.14%0.566656.108.92%0.55
bays292020.002030.000.50%0.022044.001.19%0.03
berlin527544.378244.839.28%0.088069.846.97%0.09
eil51429.98455.946.04%0.06452.175.16%0.09
ulysses168182.208176.38-0.07%0.018179.44-0.03%0.01
kroD10021294.2923441.2310.08%0.4021676.431.79%0.25
gr20260350.2156023.71-7.17%1.3355704.01-7.70%1.51
ch1506532.287531.6315.30%0.766952.566.43%0.73
a2802586.772920.0512.88%2.892812.688.73%3.39
pr76108159.44115930.237.18%0.13113672.765.10%0.17
ulysses228354.998496.751.70%0.028306.20-0.58%0.02
kroC10020750.7623680.5114.12%0.2720871.380.58%0.28
eil76545.39578.426.06%0.13572.825.03%0.13
eil101642.31672.744.74%0.34671.684.57%0.28
att4810628.0011148.004.89%0.0610859.002.17%0.06
st70678.60734.538.24%0.13702.503.52%0.16
bayg291610.001708.006.09%0.031610.000.00%0.03

对比图

 

这篇关于39、TSP的几种精确求解方法(DFJ、MTZ、最短边破圈式、行生成式)和启发式方法(插入法、近邻法、2-opt、3-opt、模拟退火)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/336084

相关文章

Linux换行符的使用方法详解

《Linux换行符的使用方法详解》本文介绍了Linux中常用的换行符LF及其在文件中的表示,展示了如何使用sed命令替换换行符,并列举了与换行符处理相关的Linux命令,通过代码讲解的非常详细,需要的... 目录简介检测文件中的换行符使用 cat -A 查看换行符使用 od -c 检查字符换行符格式转换将

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Java中的String.valueOf()和toString()方法区别小结

《Java中的String.valueOf()和toString()方法区别小结》字符串操作是开发者日常编程任务中不可或缺的一部分,转换为字符串是一种常见需求,其中最常见的就是String.value... 目录String.valueOf()方法方法定义方法实现使用示例使用场景toString()方法方法

Java中List的contains()方法的使用小结

《Java中List的contains()方法的使用小结》List的contains()方法用于检查列表中是否包含指定的元素,借助equals()方法进行判断,下面就来介绍Java中List的c... 目录详细展开1. 方法签名2. 工作原理3. 使用示例4. 注意事项总结结论:List 的 contain

macOS无效Launchpad图标轻松删除的4 种实用方法

《macOS无效Launchpad图标轻松删除的4种实用方法》mac中不在appstore上下载的应用经常在删除后它的图标还残留在launchpad中,并且长按图标也不会出现删除符号,下面解决这个问... 在 MACOS 上,Launchpad(也就是「启动台」)是一个便捷的 App 启动工具。但有时候,应

SpringBoot日志配置SLF4J和Logback的方法实现

《SpringBoot日志配置SLF4J和Logback的方法实现》日志记录是不可或缺的一部分,本文主要介绍了SpringBoot日志配置SLF4J和Logback的方法实现,文中通过示例代码介绍的非... 目录一、前言二、案例一:初识日志三、案例二:使用Lombok输出日志四、案例三:配置Logback一

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Flutter打包APK的几种方式小结

《Flutter打包APK的几种方式小结》Flutter打包不同于RN,Flutter可以在AndroidStudio里编写Flutter代码并最终打包为APK,本篇主要阐述涉及到的几种打包方式,通... 目录前言1. android原生打包APK方式2. Flutter通过原生工程打包方式3. Futte

mysql出现ERROR 2003 (HY000): Can‘t connect to MySQL server on ‘localhost‘ (10061)的解决方法

《mysql出现ERROR2003(HY000):Can‘tconnecttoMySQLserveron‘localhost‘(10061)的解决方法》本文主要介绍了mysql出现... 目录前言:第一步:第二步:第三步:总结:前言:当你想通过命令窗口想打开mysql时候发现提http://www.cpp

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T