使用近似算法解决旅行商(TSP)问题

什么是TSP问题?

谓TSP问题(Travelling Salesman Problem)旅行商问题,即最短路径问题,就是在给定的起始点S到终止点T的通路集合中,寻求距离最小的通路,这样的通路称为S点到T点的最短路径。
TSP是一种完全NP问题如果旅行商问题的权值函数满足三角不等式,即c(u,w)≤c(u,v) + c(v,w)对任意u,v,w都成立,则称它满足三角不等式。无论旅行商问题是否满足三角不等式,它均是NP-完全问题。相关定理表明,不满足三角不等式的旅行商问题不存在常数近似比的近似算法,除非NP=P。

近似算法设计

近似算法是指能够在多项式时间内给出优化问题的近似优化解的算法,近似算法不仅可用于近似求解NP-完全问题,也可用于近似求解复杂度较高的P问题。

  1. 任意选择V中的一个顶点r,作为树根节点;
  2. 调用Prim算法得到图G(V,E)的最小生成树T*;
  3. 先序遍历T,访问T中的每条边两遍,得到顶点序列L;
  4. 删除L中的重复顶点形成哈密顿环C;
  5. 输出C.
  6. 算法的性能分析

时间复杂度

近似算法的性能分析包括时间复杂度分析、空间复杂度分析和近似精度分析,其中时间(空间)复杂度的分析同精确复杂度相同。近似精度分析是近似算法特有的,它主要用于刻画近似算法给出的近似解相比于问题优化解的优劣程度。目前,存在三种刻画近似精度的度量,即近似比、相对误差界和1+ε近似。
该算法的时间复杂度为O(|V|^2 log|V|)。事实上,第2步开销为O(|E| log|V|)且图G是完全图,O(|E| log|V|)等于O(|V|^2 log|V|)。第3~4步的开销为O(|V|),因为最小生成树恰有|V| - 1条边。

近似精度

近似比:设A是一个优化问题的近似算法,A具有近似比(ratio bound) p(n), 如果max{C/C, C/C} ≤ p(n)。其中n是输入大小,C是A产生的解的代价,C是优化解的代价。
相对误差:对于任意输入,近似算法的相对误差定义为|C - C
|/C,其中C是近似解的代价,C是优化解的代价。
相对误差界:一个近似算法的相对误差界为ε(n),如果|C-C|/C ≤ ε(n)。
近似模式:一个优化问题的近似模式是一个以问题实例I和ε>0位输入的算法。对于任意固定的ε,近似模式是一个(1+ε)-近似算法。一个近似模式A(I,ε)称为一个多项式时间近似模式,如果对于任意ε>0, A(I,ε)的运行时间是|I|的多项式。一个近似模式称为完全多项式时间近似模式,如果它的运行时间是关于I/ε和输入实例大小n的多项式。
对于近似解C,由于L是遍历T的每条边两次得到的回路,则有c(L) = 2 c(T), 而C又是关于L删除某些重复边后得到的结果,因此C ≤ 2 c(T)。
对于优化解C
,由于C是一个简单环,则删除任一条边便可生成树,而且该树的代价一定不低于最小生成树的代价,因此有c(C) ≥ c(T)。
综上,有C ≤ 2
c(T) ≤ 2 C。所以C / C ≤ 2,该算法的近似比为2。

近似算法解决TSP问题过程

TSP问题的实质可以抽象为在一个带权重的完全无向图中,找到一个权值总和最小的哈密顿回路
TSP问题翻译为数学语言为,在N个城市的完全无向图G中
enter image description here

其中每个城市之间的距离矩阵为 enter image description here
目标函数为enter image description here

需要求解的变量为w,w是使得目标函数达到最小值的一个排列 enter image description here
且w的最后一项满足回到出发城市 enter image description here

满足三角不等式的TSP模型和算法步骤

我们从费用函数出发,费用函数也叫代价函数,指的是两个城市之间的费用指数或者代价程度的量化。在大多数的实际情况中,从一个地方u直接到另一个地方w,这个走法花费的代价总是最小的,而如果从u到w需要经过某个中转站v,则这种走法花费的代价却不可能比直接到达的走法花费的代价更小将上述的理论转化为数学语言为enter image description here
其中c是费用函数,这个方程说明了,直接从u->w花费的代价,要比从u->v->w花费的代价要小,我们称这个费用函数满足三角不等式三角不等式的定义为:任意一个欧拉平面的三角形两边之和始终大于第三边,这是一个非常自然的不等式,其中欧拉平面上任意两点之间的欧式距离就满足三角不等式,为此,我们只要设TSP中的费用函数为欧式距离,即可将TSP问题转化为满足三角不等式的TSP模型

近似算法的解题步骤求解上述TSP模型的步骤

(1)选择G的任意一个顶点r作为根节点(出发/结束点)
(2)用Prim算法找出G的一棵以r为根的最小生成树T
(3)前序遍历访问树T,得到遍历顺序组成的顶点表L
(4)将r加到顶点表L的末尾,按L中顶点的次序组成哈密顿回路H
数学上已经证明,当费用函数满足三角不等式时,上述近似算法找出的哈密顿回路产生的总费用,不会超过最优回路的2倍

图的存储结构

首先我们需要将图表示为我们熟悉的数据结构,图可以使用两种存储结构,分别是邻接链表和邻接矩阵邻接链表:是一个由链表组成的一维数组,数组中每个元素都存储以每个顶点为表头的链表邻接矩阵:以矩阵的形式存储图中所有顶点之间的关系用链表表示图的关系,会显得数据结构较为复杂,但节省空间的开销,而用矩阵来表示图的关系就显得非常清晰,但空间开销较大,这里我们选择邻接矩阵来表示TSP案例中的无向图G
我们设欧式距离为费用函数,矩阵中的每一行代表G中每一个的顶点到其余各个顶点的费用(欧式距离),如果出现到达不了或者自身到达自身的情况,我们用无穷大inf来填充表示不可达

1
2
3
4
5
6
7
8
9
10
11
12
def price_cn(vec1, vec2):
return np.linalg.norm(np.array(vec1) - np.array(vec2))
# 从去过的点中,找到连接到未去过的点的边里,最小的代价边(贪心算法)
def find_min_edge(visited_ids, no_visited_ids):
min_weight, min_from, min_to = np.inf, np.inf, np.inf
for from_index in visited_ids:
for to_index, weight in enumerate(G[from_index]):
if from_index != to_index and weight < min_weight and to_index in no_visited_ids:
min_to = to_index
min_from = from_index
min_weight = G[min_from][min_to]
return (min_from, min_to), min_weight

Prim最小生成树算法

两点可以确定一条直线,则最小生成树的定义为:用n-1条边连接具有n个顶点的无向图G,并且使得边长的总和最小接下来我们需要找到G中的这颗最小生成树T,从T的定义可知,T满足
(1)T有且只有n-1条边
(2)T的总长度达到最小值
这里我们使用Prim算法来生成T,Prim算法的策略步骤为
(1)设集合V是G中所有的顶点,集合U是G中已经走过的顶点,集合U-V是G中没有走过的顶点
(2)从G中的起点a开始遍历,将a加入到集合U中,并将a从集合U-V替出
(3)在集合U-V中剩余的n-1个顶点中寻找与集合U中的a关联,且权重最小的那条边的终点b,将b加入到集合U中,并将b从集合U-V替出
(4)同理,在集合U-V中剩余的n-2个顶点中寻找与集合U中的a或b关联,且权重最小的那条边的终点c,将c加入到集合U中,并将c从集合U-V替出
(5)重复步骤(4),直到G中所有的顶点都加入到集合U,且集合U-V为空,则集合U中的顶点就构成了T
显然,Prim算法的策略属于贪心算法,因为每一步所加入的边,都必须是使得当前数T的总权重增加量最小的边

1
2
3
4
5
6
7
8
9
10
11
12
13
def prim(G, root_index=0):
visited_ids = [root_index] # 初始化去过的点的集合
T_path = []
while len(visited_ids) != G.shape[0]:
no_visited_ids = contain_no_visited_ids(G, visited_ids) # 维护未去过的点的集合
(min_from, min_to), min_weight = find_min_edge(visited_ids, no_visited_ids)
visited_ids.append(min_to) # 维护去过的点的集合
T_path.append((min_from, min_to))
T = np.full_like(G, np.inf) # 最小生成树的矩阵形式,n-1条边组成
for (from_, to_) in T_path:
T[from_][to_] = G[from_][to_]
T[to_][from_] = G[to_][from_]
return T, T_path

遍历

序遍历:访问根节点—>前序遍历左子树—>前序遍历右子树

1
2
3
4
5
6
7
8
9
10
11
12
def preorder_tree_walk(T, root_index=0):
is_visited = [False] * T.shape[0]
stack = [root_index]
T_walk = []
while len(stack) != 0:
node = stack.pop()
T_walk.append(node)
is_visited[node] = True
nodes = np.where(T[node] != np.inf)[0]
if len(nodes) > 0:
[stack.append(node) for node in reversed(nodes) if is_visited[node] is False]
return T_walk

哈密顿回路

哈密顿回路的定义为:由指定的起点前往指定的终点,途中经过的城市有且只经过一次,所以一个无向图中含有若干个哈密顿回路
按照近似算法的最后一步,我们将根节点加入到顶点表的末尾,将顶点表的顶点顺序依次连接,就得到哈密顿回路

1
2
3
4
5
6
7
8
9
def create_H(G, L):
H = np.full_like(G, np.inf)
H_path = []
for i, from_node in enumerate(L[0:-1]):
to_node = L[i + 1]
H[from_node][to_node] = G[from_node][to_node]
H[to_node][from_node] = G[to_node][from_node]
H_path.append((from_node, to_node))
return H, H_path

完成了近似算法的计算,找到了一个在G中从a出发,最后回到a,中间每个城市只经过一次的最小费用的行程走法,即计算出了目标函数的顶点排列w为enter image description here

结果分析

从结果上可以看出,近似算法是解决TSP问题的一种有效方法,它可以在多项式时间内计算出一个近似解,来逼近真实的最优解,这个近似解尽量的逼近满足TSP的条件
(1)从开始点回到开始点,每个点都要经过且只经过一次
(2)行程的总费用达到最小值
近似算法求解TSP问题
(3)近似算法是求解TSP问题的一个渐进式算法
(4)近似解法求出的近似解和实际最优解的近似比不超过2,即w的总代价,在最优总代价的2倍之内

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
'''
TSP—近似算法
1、选择G的任意一个顶点
2、Prim算法找出找出最小生成树T
3、前序遍历树T得到的顶点表L
4、将根节点添加到L的末尾,按表L中顶点的次序组成哈密顿回路H
'''
import numpy as np
import matplotlib.pyplot as plt
# 代价函数(具有三角不等式性质)
def price_cn(vec1, vec2):
return np.linalg.norm(np.array(vec1) - np.array(vec2))
# 从去过的点中,找到连接到未去过的点的边里,最小的代价边(贪心算法)
def find_min_edge(visited_ids, no_visited_ids):
min_weight, min_from, min_to = np.inf, np.inf, np.inf
for from_index in visited_ids:
for to_index, weight in enumerate(G[from_index]):
if from_index != to_index and weight < min_weight and to_index in no_visited_ids:
min_to = to_index
min_from = from_index
min_weight = G[min_from][min_to]
return (min_from, min_to), min_weight
# 维护未走过的点的集合
def contain_no_visited_ids(G, visited_ids):
no_visited_ids = [] # 还没有走过的点的索引集合
[no_visited_ids.append(idx) for idx, _ in enumerate(G) if idx not in visited_ids]
return no_visited_ids
# 生成最小生成树T
def prim(G, root_index=0):
visited_ids = [root_index] # 初始化去过的点的集合
T_path = []
while len(visited_ids) != G.shape[0]:
no_visited_ids = contain_no_visited_ids(G, visited_ids) # 维护未去过的点的集合
(min_from, min_to), min_weight = find_min_edge(visited_ids, no_visited_ids)
visited_ids.append(min_to) # 维护去过的点的集合
T_path.append((min_from, min_to))
T = np.full_like(G, np.inf) # 最小生成树的矩阵形式,n-1条边组成
for (from_, to_) in T_path:
T[from_][to_] = G[from_][to_]
T[to_][from_] = G[to_][from_]
return T, T_path
# 先序遍历图(最小生成树)的路径,得到顶点列表L
def preorder_tree_walk(T, root_index=0):
is_visited = [False] * T.shape[0]
stack = [root_index]
T_walk = []
while len(stack) != 0:
node = stack.pop()
T_walk.append(node)
is_visited[node] = True
nodes = np.where(T[node] != np.inf)[0]
if len(nodes) > 0:
[stack.append(node) for node in reversed(nodes) if is_visited[node] is False]
return T_walk
# 生成哈密尔顿回路H
def create_H(G, L):
H = np.full_like(G, np.inf)
H_path = []
for i, from_node in enumerate(L[0:-1]):
to_node = L[i + 1]
H[from_node][to_node] = G[from_node][to_node]
H[to_node][from_node] = G[to_node][from_node]
H_path.append((from_node, to_node))
return H, H_path
# 可视化画出哈密顿回路
def draw_H(citys, H_path):
fig = plt.figure()
ax = fig.add_subplot(111)
plt.xlim(0, 7)
plt.ylim(0, 7)
for (from_, to_) in H_path:
p1 = plt.Circle(citys[from_], 0.2, color='red')
p2 = plt.Circle(citys[to_], 0.2, color='red')
ax.add_patch(p1)
ax.add_patch(p2)
ax.plot((citys[from_][0], citys[to_][0]), (citys[from_][1], citys[to_][1]), color='red')
ax.annotate(s=chr(97 + to_), xy=citys[to_], xytext=(-8, -4), textcoords='offset points', fontsize=20)
ax.axis('equal')
ax.grid()
plt.show()
if __name__ == '__main__':
citys = [(2, 6), (2, 4), (1, 3), (4, 6), (5, 5), (4, 4), (6, 4), (3, 2)] # 城市坐标
G = [] # 完全无向图
for i, curr_point in enumerate(citys):
line = []
for j, other_point in enumerate(citys):
line.append(price_cn(curr_point, other_point)) if i != j else line.append(np.inf)
G.append(line)
G = np.array(G)
# 1、选择G的任意一个顶点
root_index = 0
# 2、Prim算法找出找出最小生成树T
T, T_path = prim(G, root_index=root_index)
# 3、前序遍历树T得到的顶点表L
L = preorder_tree_walk(T, root_index=root_index)
# 4、将根节点添加到L的末尾,按表L中顶点的次序组成哈密顿回路H
L.append(root_index)
H, H_path = create_H(G, L)
print('最小生成树的路径为:{}'.format(T_path))
[print(chr(97 + v), end=',' if i < len(L) - 1 else '\n') for i, v in enumerate(L)]
print('哈密顿回路的路径为:{}'.format(H_path))
print('哈密顿回路产生的代价为:{}'.format(sum(G[from_][to_] for (from_, to_) in H_path)))
# draw_H(citys, H_path)
× 请我吃糖~
打赏二维码