1
- """ Travelling Salesman Problem (TSP) """
1
+ """Travelling Salesman Problem (TSP)"""
2
2
3
3
import itertools
4
4
import math
5
5
6
+
6
7
class InvalidGraphError (ValueError ):
7
8
"""Custom error for invalid graph inputs."""
8
9
10
+
9
11
def euclidean_distance (point1 : list [float ], point2 : list [float ]) -> float :
10
12
"""
11
13
Calculate the Euclidean distance between two points in 2D space.
@@ -28,6 +30,7 @@ def euclidean_distance(point1: list[float], point2: list[float]) -> float:
28
30
except TypeError :
29
31
raise ValueError ("Invalid input: Points must be numerical coordinates" )
30
32
33
+
31
34
def validate_graph (graph_points : dict [str , list [float ]]) -> None :
32
35
"""
33
36
Validate the input graph to ensure it has valid nodes and coordinates.
@@ -41,12 +44,12 @@ def validate_graph(graph_points: dict[str, list[float]]) -> None:
41
44
Traceback (most recent call last):
42
45
...
43
46
InvalidGraphError: Each node must have a valid 2D coordinate [x, y]
44
-
47
+
45
48
>>> validate_graph([10, 20]) # Invalid input type
46
49
Traceback (most recent call last):
47
50
...
48
51
InvalidGraphError: Graph must be a dictionary with node names and coordinates
49
-
52
+
50
53
>>> validate_graph({"A": [10, 20], "B": [30, 21], "C": [15]}) # Missing coordinate
51
54
Traceback (most recent call last):
52
55
...
@@ -66,6 +69,7 @@ def validate_graph(graph_points: dict[str, list[float]]) -> None:
66
69
):
67
70
raise InvalidGraphError ("Each node must have a valid 2D coordinate [x, y]" )
68
71
72
+
69
73
# TSP in Brute Force Approach
70
74
def travelling_salesman_brute_force (
71
75
graph_points : dict [str , list [float ]],
@@ -89,7 +93,7 @@ def travelling_salesman_brute_force(
89
93
raise InvalidGraphError ("Graph must have at least two nodes" )
90
94
91
95
min_path = [] # List that stores shortest path
92
- min_distance = float ("inf" ) # Initialize minimum distance to infinity
96
+ min_distance = float ("inf" ) # Initialize minimum distance to infinity
93
97
94
98
start_node = nodes [0 ]
95
99
other_nodes = nodes [1 :]
@@ -111,6 +115,7 @@ def travelling_salesman_brute_force(
111
115
112
116
return min_path , min_distance
113
117
118
+
114
119
# TSP in Dynamic Programming approach
115
120
def travelling_salesman_dynamic_programming (
116
121
graph_points : dict [str , list [float ]],
@@ -127,20 +132,26 @@ def travelling_salesman_dynamic_programming(
127
132
"""
128
133
validate_graph (graph_points )
129
134
130
- n = len (graph_points ) # Extracting the node names (keys)
135
+ n = len (graph_points ) # Extracting the node names (keys)
131
136
132
137
# There shoukd be atleast 2 nodes for a valid TSP
133
138
if n < 2 :
134
139
raise InvalidGraphError ("Graph must have at least two nodes" )
135
140
136
- nodes = list (graph_points .keys ()) # Extracting the node names (keys)
141
+ nodes = list (graph_points .keys ()) # Extracting the node names (keys)
137
142
138
143
# Initialize distance matrix with float values
139
- dist = [[euclidean_distance (graph_points [nodes [i ]], graph_points [nodes [j ]]) for j in range (n )] for i in range (n )]
140
-
141
- # Initialize a dynamic programming table with infinity
144
+ dist = [
145
+ [
146
+ euclidean_distance (graph_points [nodes [i ]], graph_points [nodes [j ]])
147
+ for j in range (n )
148
+ ]
149
+ for i in range (n )
150
+ ]
151
+
152
+ # Initialize a dynamic programming table with infinity
142
153
dp = [[float ("inf" )] * n for _ in range (1 << n )]
143
- dp [1 ][0 ] = 0 # Only visited node is the starting point at node 0
154
+ dp [1 ][0 ] = 0 # Only visited node is the starting point at node 0
144
155
145
156
# Iterate through all masks of visited nodes
146
157
for mask in range (1 << n ):
@@ -149,14 +160,16 @@ def travelling_salesman_dynamic_programming(
149
160
if mask & (1 << u ):
150
161
# Traverse nodes 'v' such that u->v
151
162
for v in range (n ):
152
- if mask & (1 << v ) == 0 : # If v is not visited
153
- next_mask = mask | (1 << v ) # Upodate mask to include 'v'
163
+ if mask & (1 << v ) == 0 : # If v is not visited
164
+ next_mask = mask | (1 << v ) # Upodate mask to include 'v'
154
165
# Update dynamic programming table with minimum distance
155
- dp [next_mask ][v ] = min (dp [next_mask ][v ], dp [mask ][u ] + dist [u ][v ])
166
+ dp [next_mask ][v ] = min (
167
+ dp [next_mask ][v ], dp [mask ][u ] + dist [u ][v ]
168
+ )
156
169
157
170
final_mask = (1 << n ) - 1
158
171
min_cost = float ("inf" )
159
- end_node = - 1 # Track the last node in the optimal path
172
+ end_node = - 1 # Track the last node in the optimal path
160
173
161
174
for u in range (1 , n ):
162
175
if min_cost > dp [final_mask ][u ] + dist [u ][0 ]:
@@ -175,7 +188,7 @@ def travelling_salesman_dynamic_programming(
175
188
== dp [mask ^ (1 << end_node )][u ] + dist [u ][end_node ]
176
189
):
177
190
mask ^= 1 << end_node # Update mask to remove end node
178
- end_node = u # Set the previous node as end node
191
+ end_node = u # Set the previous node as end node
179
192
break
180
193
181
194
path .append (nodes [0 ]) # Bottom-up Order
0 commit comments