Skip to content

Commit 672ec7a

Browse files
committed
Add 2-opt-tsp solver
1 parent 86b9fb2 commit 672ec7a

File tree

3 files changed

+142
-0
lines changed

3 files changed

+142
-0
lines changed

2-opt-tsp/2-opt-tsp.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import numpy as np
2+
3+
# convert A of input to 0, when output
4+
5+
class TSP:
6+
def __init__(self, weights, tour, verticesMapping):
7+
self.tour = tour # preorder traversal of MST, ordered list of ints
8+
self.weights = weights # np 2D array of ints, cost matrix, keys are ints
9+
self.d = self.calculateWeight(tour)
10+
self.verticesMapping = verticesMapping
11+
print(f"Tour: {self.printTour(self.tour)}, total distance: {self.d}")
12+
13+
# weight of the input tour
14+
def calculateWeight(self, tour):
15+
ret = 0
16+
for idx in range(len(tour)):
17+
v_1 = tour[idx]
18+
if idx == len(tour)-1:
19+
v_2 = tour[0]
20+
else:
21+
v_2 = tour[idx+1]
22+
ret += self.weights[v_1][v_2]
23+
return ret
24+
25+
# optimize
26+
def optimize(self):
27+
N = len(self.tour)
28+
for i in range(N-3):
29+
for j in range(i+2, N-1):
30+
newTour = np.copy(self.tour)
31+
newTour[i+1] = self.tour[j]
32+
tmp = i+2
33+
for k in range(j-1, i, -1):
34+
newTour[tmp] = self.tour[k]
35+
tmp += 1
36+
newD = self.calculateWeight(newTour)
37+
if newD < self.d:
38+
self.tour = np.copy(newTour)
39+
self.d = newD
40+
print(f"Tour: {self.printTour(self.tour)}, total distance: {self.d}")
41+
self.optimize() # tail recursive
42+
43+
# map a list of integers to the corresponding list of strings
44+
def printTour(self, tour):
45+
return list(map(lambda x: self.verticesMapping[x], tour))
46+
from collections import defaultdict
47+
from typing import DefaultDict
48+
import heapq
49+
class MST:
50+
def __init__(self, cost, root):
51+
self.root = root
52+
self.cost_matrix = cost
53+
self.mst = self.prims()
54+
55+
def prims(self):
56+
ret = defaultdict(set)
57+
visited = set([self.root])
58+
edges = [(cost, self.root, v_2) for v_2, cost in enumerate(self.cost_matrix[self.root])]
59+
heapq.heapify(edges)
60+
while edges:
61+
cost, start, to = heapq.heappop(edges)
62+
if to not in visited:
63+
visited.add(to)
64+
ret[start].add(to)
65+
for to_next, cost in enumerate(self.cost_matrix[to]):
66+
if to_next not in visited:
67+
heapq.heappush(edges, (self.cost_matrix[to][to_next], to, to_next))
68+
return ret
69+
70+
def preorder(self, root):
71+
# convert mst a dictionary to list
72+
children = self.mst[root]
73+
ret = [root]
74+
for child in children:
75+
ret = ret + self.preorder(child)
76+
return ret
77+
78+
import argparse
79+
80+
if __name__ == '__main__':
81+
parser = argparse.ArgumentParser(description="Apply 2-opt algorithm to approximate TSP solution step by step. By Roundofthree.")
82+
# ask for vertices, eg.: A,B,C,D,E
83+
vertices = input("Enter the vertices separated by single comma: ").strip().split(",") # ['A', 'B', 'C', 'D', 'E']
84+
verticesMapping = []
85+
N = len(vertices)
86+
for i in range(0, N):
87+
verticesMapping.append(vertices[i])
88+
# ask for cost matrix nxn, eg.:
89+
# A | B | C | D | E
90+
# A -> 0,13,23,4,2,4
91+
# B ->
92+
print(' | '.join(vertices))
93+
cost = []
94+
for v in vertices:
95+
input_array = list(map(int, input(v+"-> ").strip().split(",")))
96+
if len(input_array) == N:
97+
cost.append(input_array)
98+
else:
99+
print("error") # and terminate
100+
np_cost = np.array(cost).reshape(N,N)
101+
# calculate MST from np_cost and vertices --> Prim's starting from 0
102+
mst = MST(np_cost, 0)
103+
mst_preorder = mst.preorder(0)
104+
# mst_preorder = [0, 1, 2, 3, 4]
105+
# compute preorder traversal of tree as a list
106+
tsp_solver = TSP(np_cost, mst_preorder, verticesMapping)
107+
tsp_solver.optimize()
108+
109+
110+
111+
112+
113+

2-opt-tsp/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## 2-opt TSP algorithm
2+
3+
Input: weighted complete graph
4+
5+
```
6+
Enter the vertices separated by single comma: A,B,C,D,E
7+
A | B | C | D | E
8+
A-> 0,1,1,1,1 # enter the cost matrix as an adjacency matrix, separated by single comma
9+
B-> 1,0,1,1,1
10+
C-> 1,1,0,1,1
11+
D-> 1,1,1,0,1
12+
E-> 1,1,1,1,0
13+
```
14+
15+
Output: Hamiltonian cycle, with step-by-step swaps
16+
17+
```
18+
Tour: ['A', 'B', 'C', 'D', 'E'], total distance: 5
19+
```
20+
21+
Procedure:
22+
23+
1. Construct Minimum Spanning Tree with Prim's algorithm.
24+
2. Preorder traversal, and set the `score` to the weight sum.
25+
3. Heuristic: find the largest edge first, then other edges.
26+
1. Choose another edge.
27+
2. Cross: (a, b) with (c, d) --> (a,c)
28+
3. If total weight of new tour is smaller, update tour and start from 3 again.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
| Script | Description |
44
| -- | -- |
55
|[Signature parser](create_electronic_sign) | Parse handwritten signature in JPG/JPEG/PNG to a new JPG with black signature and transparent background. |
6+
|[2-opt TSP](2-opt-tsp) | Find local minimum weighted Hamiltonian cycle in a complete graph expressed with a cost adjacency matrix. |

0 commit comments

Comments
 (0)