-
-
Notifications
You must be signed in to change notification settings - Fork 46.6k
/
Copy pathtravelling_salesman_problem.py
239 lines (192 loc) · 7.56 KB
/
travelling_salesman_problem.py
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""Travelling Salesman Problem (TSP)"""
import itertools
import math
class InvalidGraphError(ValueError):
"""Custom error for invalid graph inputs."""
def euclidean_distance(point1: list[float], point2: list[float]) -> float:
"""
Calculate the Euclidean distance between two points in 2D space.
:param point1: Coordinates of the first point [x, y]
:param point2: Coordinates of the second point [x, y]
:return: The Euclidean distance between the two points
>>> euclidean_distance([0, 0], [3, 4])
5.0
>>> euclidean_distance([1, 1], [1, 1])
0.0
>>> euclidean_distance([1, 1], ['a', 1])
Traceback (most recent call last):
...
ValueError: Invalid input: Points must be numerical coordinates
"""
try:
return math.sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2)
except TypeError:
raise ValueError("Invalid input: Points must be numerical coordinates")
def validate_graph(graph_points: dict[str, list[float]]) -> None:
"""
Validate the input graph to ensure it has valid nodes and coordinates.
:param graph_points: A dictionary where the keys are node names,
and values are 2D coordinates as [x, y]
:raises InvalidGraphError: If the graph points are not valid
>>> validate_graph({"A": [10, 20], "B": [30, 21], "C": [15, 35]}) # Valid graph
>>> validate_graph({"A": [10, 20], "B": [30, "invalid"], "C": [15, 35]})
Traceback (most recent call last):
...
InvalidGraphError: Each node must have a valid 2D coordinate [x, y]
>>> validate_graph([10, 20]) # Invalid input type
Traceback (most recent call last):
...
InvalidGraphError: Graph must be a dictionary with node names and coordinates
>>> validate_graph({"A": [10, 20], "B": [30, 21], "C": [15]}) # Missing coordinate
Traceback (most recent call last):
...
InvalidGraphError: Each node must have a valid 2D coordinate [x, y]
"""
if not isinstance(graph_points, dict):
raise InvalidGraphError(
"Graph must be a dictionary with node names and coordinates"
)
for node, coordinates in graph_points.items():
if (
not isinstance(node, str)
or not isinstance(coordinates, list)
or len(coordinates) != 2
or not all(isinstance(c, (int, float)) for c in coordinates)
):
raise InvalidGraphError("Each node must have a valid 2D coordinate [x, y]")
# TSP in Brute Force Approach
def travelling_salesman_brute_force(
graph_points: dict[str, list[float]],
) -> tuple[list[str], float]:
"""
Solve the Travelling Salesman Problem using brute force.
:param graph_points: A dictionary of nodes and their coordinates {node: [x, y]}
:return: The shortest path and its total distance
>>> graph = {"A": [10, 20], "B": [30, 21], "C": [15, 35]}
>>> travelling_salesman_brute_force(graph)
(['A', 'C', 'B', 'A'], 56.35465722402587)
"""
validate_graph(graph_points)
nodes = list(graph_points.keys()) # Extracting the node names (keys)
# There shoukd be atleast 2 nodes for a valid TSP
if len(nodes) < 2:
raise InvalidGraphError("Graph must have at least two nodes")
min_path = [] # List that stores shortest path
min_distance = float("inf") # Initialize minimum distance to infinity
start_node = nodes[0]
other_nodes = nodes[1:]
# Iterating over all permutations of the other nodes
for perm in itertools.permutations(other_nodes):
path = [start_node, *perm, start_node]
# Calculating the total distance
total_distance = sum(
euclidean_distance(graph_points[path[i]], graph_points[path[i + 1]])
for i in range(len(path) - 1)
)
# Update minimum distance if shorter path found
if total_distance < min_distance:
min_distance = total_distance
min_path = path
return min_path, min_distance
# TSP in Dynamic Programming approach
def travelling_salesman_dynamic_programming(
graph_points: dict[str, list[float]],
) -> tuple[list[str], float]:
"""
Solve the Travelling Salesman Problem using dynamic programming.
:param graph_points: A dictionary of nodes and their coordinates {node: [x, y]}
:return: The shortest path and its total distance
>>> graph = {"A": [10, 20], "B": [30, 21], "C": [15, 35]}
>>> travelling_salesman_dynamic_programming(graph)
(['A', 'C', 'B', 'A'], 56.35465722402587)
"""
validate_graph(graph_points)
n = len(graph_points) # Extracting the node names (keys)
# There shoukd be atleast 2 nodes for a valid TSP
if n < 2:
raise InvalidGraphError("Graph must have at least two nodes")
nodes = list(graph_points.keys()) # Extracting the node names (keys)
# Initialize distance matrix with float values
dist = [
[
euclidean_distance(graph_points[nodes[i]], graph_points[nodes[j]])
for j in range(n)
]
for i in range(n)
]
# Initialize a dynamic programming table with infinity
dp = [[float("inf")] * n for _ in range(1 << n)]
dp[1][0] = 0 # Only visited node is the starting point at node 0
# Iterate through all masks of visited nodes
for mask in range(1 << n):
for u in range(n):
# If current node 'u' is visited
if mask & (1 << u):
# Traverse nodes 'v' such that u->v
for v in range(n):
if mask & (1 << v) == 0: # If v is not visited
next_mask = mask | (1 << v) # Upodate mask to include 'v'
# Update dynamic programming table with minimum distance
dp[next_mask][v] = min(
dp[next_mask][v], dp[mask][u] + dist[u][v]
)
final_mask = (1 << n) - 1
min_cost = float("inf")
end_node = -1 # Track the last node in the optimal path
for u in range(1, n):
if min_cost > dp[final_mask][u] + dist[u][0]:
min_cost = dp[final_mask][u] + dist[u][0]
end_node = u
path = []
mask = final_mask
while end_node != 0:
path.append(nodes[end_node])
for u in range(n):
# If current state corresponds to optimal state before visiting end node
if (
mask & (1 << u)
and dp[mask][end_node]
== dp[mask ^ (1 << end_node)][u] + dist[u][end_node]
):
mask ^= 1 << end_node # Update mask to remove end node
end_node = u # Set the previous node as end node
break
path.append(nodes[0]) # Bottom-up Order
path.reverse() # Top-Down Order
path.append(nodes[0])
return path, min_cost
# Demo Graph
# C (15, 35)
# |
# |
# |
# F (5, 15) --- A (10, 20)
# | |
# | |
# | |
# | |
# E (25, 5) --- B (30, 21)
# |
# |
# |
# D (40, 10)
# |
# |
# |
# G (50, 25)
if __name__ == "__main__":
demo_graph = {
"A": [10.0, 20.0],
"B": [30.0, 21.0],
"C": [15.0, 35.0],
"D": [40.0, 10.0],
"E": [25.0, 5.0],
"F": [5.0, 15.0],
"G": [50.0, 25.0],
}
# Brute force
brute_force_result = travelling_salesman_brute_force(demo_graph)
print(f"Brute force result: {brute_force_result}")
# Dynamic programming
dp_result = travelling_salesman_dynamic_programming(demo_graph)
print(f"Dynamic programming result: {dp_result}")