1
- """
2
- title : Travelling Sales man Problem
3
- references : https://en.wikipedia.org/wiki/Travelling_salesman_problem
4
- author : SunayBhoyar
5
- """
6
-
1
+ #!/usr/bin/env python3
7
2
import itertools
8
3
import math
9
4
10
- demo_graph_points = {
11
- "A" : [10 , 20 ],
12
- "B" : [30 , 21 ],
13
- "C" : [15 , 35 ],
14
- "D" : [40 , 10 ],
15
- "E" : [25 , 5 ],
16
- "F" : [5 , 15 ],
17
- "G" : [50 , 25 ],
18
- }
5
+
6
+ class InvalidGraphError (ValueError ):
7
+ """Custom error for invalid graph inputs."""
19
8
20
9
21
- # Euclidean distance - shortest distance between 2 points
22
- def distance (point1 , point2 ):
10
+ def euclidean_distance (point1 : list [float ], point2 : list [float ]) -> float :
23
11
"""
24
- Calculate the Euclidean distance between two points.
25
- @input: point1, point2 (coordinates of two points as lists [x, y])
26
- @return: Euclidean distance between point1 and point2
27
- @example:
28
- >>> distance([0, 0], [3, 4])
12
+ Calculate the Euclidean distance between two points in 2D space.
13
+
14
+ :param point1: Coordinates of the first point [x, y]
15
+ :param point2: Coordinates of the second point [x, y]
16
+ :return: The Euclidean distance between the two points
17
+
18
+ >>> euclidean_distance([0, 0], [3, 4])
29
19
5.0
20
+ >>> euclidean_distance([1, 1], [1, 1])
21
+ 0.0
22
+ >>> euclidean_distance([1, 1], ['a', 1])
23
+ Traceback (most recent call last):
24
+ ...
25
+ ValueError: Invalid input: Points must be numerical coordinates
30
26
"""
31
- return math .sqrt ((point2 [0 ] - point1 [0 ]) ** 2 + (point2 [1 ] - point1 [1 ]) ** 2 )
27
+ try :
28
+ return math .sqrt ((point2 [0 ] - point1 [0 ]) ** 2 + (point2 [1 ] - point1 [1 ]) ** 2 )
29
+ except TypeError :
30
+ raise ValueError ("Invalid input: Points must be numerical coordinates" )
32
31
33
32
34
- """
35
- Brute force
36
- Time Complexity - O(n!×n)
37
- Space Complexity - O(n)
38
- """
39
-
33
+ def validate_graph (graph_points : dict [str , list [float ]]) -> None :
34
+ """
35
+ Validate the input graph to ensure it has valid nodes and coordinates.
40
36
41
- def travelling_sales_man_problem_brute_force (graph_points ):
37
+ :param graph_points: A dictionary where the keys are node names,
38
+ and values are 2D coordinates as [x, y]
39
+ :raises InvalidGraphError: If the graph points are not valid
40
+ """
41
+ if not isinstance (graph_points , dict ):
42
+ raise InvalidGraphError (
43
+ "Graph must be a dictionary with node names and coordinates"
44
+ )
45
+
46
+ for node , coordinates in graph_points .items ():
47
+ if (
48
+ not isinstance (node , str )
49
+ or not isinstance (coordinates , list )
50
+ or len (coordinates ) != 2
51
+ ):
52
+ raise InvalidGraphError ("Each node must have a valid 2D coordinate [x, y]" )
53
+
54
+
55
+ def travelling_salesman_brute_force (
56
+ graph_points : dict [str , list [float ]],
57
+ ) -> tuple [list [str ], float ]:
42
58
"""
43
- Solve the Travelling Salesman Problem using brute force (permutations).
44
- @input: graph_points (dictionary with node names as keys and coordinates as values)
45
- @return: shortest path, total distance (list of nodes representing the shortest path and the distance of that path)
46
- @example:
47
- >>> travelling_sales_man_problem_brute_force({'A': [0, 0], 'B': [0, 1], 'C': [1, 0]})
48
- (['A', 'B', 'C', 'A'], 3.414)
59
+ Solve the Travelling Salesman Problem using brute force.
60
+
61
+ :param graph_points: A dictionary of nodes and their coordinates {node: [x, y]}
62
+ :return: The shortest path and its total distance
63
+
64
+ >>> graph = {"A": [10, 20], "B": [30, 21], "C": [15, 35]}
65
+ >>> travelling_salesman_brute_force(graph)
66
+ (['A', 'B', 'C', 'A'], 65.52370249788875)
67
+ >>> travelling_salesman_brute_force({})
68
+ Traceback (most recent call last):
69
+ ...
70
+ InvalidGraphError: Graph must have at least two nodes
49
71
"""
72
+ validate_graph (graph_points )
73
+
50
74
nodes = list (graph_points .keys ())
75
+ if len (nodes ) < 2 :
76
+ raise InvalidGraphError ("Graph must have at least two nodes" )
51
77
52
- min_path = None
78
+ min_path = []
53
79
min_distance = float ("inf" )
54
80
55
- # Considering the first Node as the start position
56
81
start_node = nodes [0 ]
57
82
other_nodes = nodes [1 :]
58
83
59
84
for perm in itertools .permutations (other_nodes ):
60
- path = [start_node ] + list (perm ) + [ start_node ]
61
- total_distance = 0
85
+ path = [start_node , * list (perm ), start_node ]
86
+ total_distance = 0.0
62
87
for i in range (len (path ) - 1 ):
63
- total_distance += distance (graph_points [path [i ]], graph_points [path [i + 1 ]])
88
+ total_distance += euclidean_distance (
89
+ graph_points [path [i ]], graph_points [path [i + 1 ]]
90
+ )
64
91
65
92
if total_distance < min_distance :
66
93
min_distance = total_distance
@@ -69,36 +96,42 @@ def travelling_sales_man_problem_brute_force(graph_points):
69
96
return min_path , min_distance
70
97
71
98
72
- """
73
- dynamic_programming
74
- Time Complexity - O(n^2×2^n)
75
- Space Complexity - O(n×2^n)
76
- """
77
-
78
-
79
- def travelling_sales_man_problem_dp (graph_points ):
99
+ def travelling_salesman_dynamic_programming (
100
+ graph_points : dict [str , list [float ]],
101
+ ) -> tuple [list [str ], float ]:
80
102
"""
81
103
Solve the Travelling Salesman Problem using dynamic programming.
82
- @input: graph_points (dictionary with node names as keys and coordinates as values)
83
- @return: shortest path, total distance (list of nodes representing the shortest path and the distance of that path)
84
- @example:
85
- >>> travelling_sales_man_problem_dp({'A': [0, 0], 'B': [0, 1], 'C': [1, 0]})
86
- (['A', 'B', 'C', 'A'], 3.414)
104
+
105
+ :param graph_points: A dictionary of nodes and their coordinates {node: [x, y]}
106
+ :return: The shortest path and its total distance
107
+
108
+ >>> graph = {"A": [10, 20], "B": [30, 21], "C": [15, 35]}
109
+ >>> travelling_salesman_dynamic_programming(graph)
110
+ (['A', 'B', 'C', 'A'], 65.52370249788875)
111
+ >>> travelling_salesman_dynamic_programming({})
112
+ Traceback (most recent call last):
113
+ ...
114
+ InvalidGraphError: Graph must have at least two nodes
87
115
"""
116
+ validate_graph (graph_points )
117
+
88
118
n = len (graph_points )
119
+ if n < 2 :
120
+ raise InvalidGraphError ("Graph must have at least two nodes" )
121
+
89
122
nodes = list (graph_points .keys ())
90
123
91
- # Precompute distances between every pair of nodes
92
- dist = [[0 ] * n for _ in range (n )]
124
+ # Initialize distance matrix with float values
125
+ dist = [[0.0 ] * n for _ in range (n )]
93
126
for i in range (n ):
94
127
for j in range (n ):
95
- dist [i ][j ] = distance (graph_points [nodes [i ]], graph_points [nodes [j ]])
128
+ dist [i ][j ] = euclidean_distance (
129
+ graph_points [nodes [i ]], graph_points [nodes [j ]]
130
+ )
96
131
97
- # dp[mask][i] represents the minimum distance to visit all nodes in the 'mask' set, ending at node i
98
132
dp = [[float ("inf" )] * n for _ in range (1 << n )]
99
- dp [1 ][0 ] = 0 # Start at node 0
133
+ dp [1 ][0 ] = 0
100
134
101
- # Iterate over all subsets of nodes (represented by mask)
102
135
for mask in range (1 << n ):
103
136
for u in range (n ):
104
137
if mask & (1 << u ):
@@ -109,18 +142,15 @@ def travelling_sales_man_problem_dp(graph_points):
109
142
dp [next_mask ][v ], dp [mask ][u ] + dist [u ][v ]
110
143
)
111
144
112
- # Reconstruct the path and find the minimum distance to return to the start
113
145
final_mask = (1 << n ) - 1
114
146
min_cost = float ("inf" )
115
147
end_node = - 1
116
148
117
- # Find the minimum distance from any node back to the starting node
118
149
for u in range (1 , n ):
119
150
if min_cost > dp [final_mask ][u ] + dist [u ][0 ]:
120
151
min_cost = dp [final_mask ][u ] + dist [u ][0 ]
121
152
end_node = u
122
153
123
- # Reconstruct the path using the dp table
124
154
path = []
125
155
mask = final_mask
126
156
while end_node != 0 :
@@ -142,14 +172,20 @@ def travelling_sales_man_problem_dp(graph_points):
142
172
143
173
144
174
if __name__ == "__main__" :
145
- print (f"Travelling salesman problem solved using Brute Force:" )
146
- path , distance_travelled = travelling_sales_man_problem_brute_force (
147
- demo_graph_points
148
- )
149
- print (f"Shortest path: { path } " )
150
- print (f"Total distance: { distance_travelled :.2f} " )
151
-
152
- print (f"\n Travelling salesman problem solved using Dynamic Programming:" )
153
- path , distance_travelled = travelling_sales_man_problem_dp (demo_graph_points )
154
- print (f"Shortest path: { path } " )
155
- print (f"Total distance: { distance_travelled :.2f} " )
175
+ demo_graph = {
176
+ "A" : [10.0 , 20.0 ],
177
+ "B" : [30.0 , 21.0 ],
178
+ "C" : [15.0 , 35.0 ],
179
+ "D" : [40.0 , 10.0 ],
180
+ "E" : [25.0 , 5.0 ],
181
+ "F" : [5.0 , 15.0 ],
182
+ "G" : [50.0 , 25.0 ],
183
+ }
184
+
185
+ # Example usage for brute force
186
+ brute_force_result = travelling_salesman_brute_force (demo_graph )
187
+ print (f"Brute force result: { brute_force_result } " )
188
+
189
+ # Example usage for dynamic programming
190
+ dp_result = travelling_salesman_dynamic_programming (demo_graph )
191
+ print (f"Dynamic programming result: { dp_result } " )
0 commit comments