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
72import itertools
83import math
94
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."""
198
209
21- # Euclidean distance - shortest distance between 2 points
22- def distance (point1 , point2 ):
10+ def euclidean_distance (point1 : list [float ], point2 : list [float ]) -> float :
2311 """
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])
2919 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
3026 """
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" )
3231
3332
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.
4036
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 ]:
4258 """
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
4971 """
72+ validate_graph (graph_points )
73+
5074 nodes = list (graph_points .keys ())
75+ if len (nodes ) < 2 :
76+ raise InvalidGraphError ("Graph must have at least two nodes" )
5177
52- min_path = None
78+ min_path = []
5379 min_distance = float ("inf" )
5480
55- # Considering the first Node as the start position
5681 start_node = nodes [0 ]
5782 other_nodes = nodes [1 :]
5883
5984 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
6287 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+ )
6491
6592 if total_distance < min_distance :
6693 min_distance = total_distance
@@ -69,36 +96,42 @@ def travelling_sales_man_problem_brute_force(graph_points):
6996 return min_path , min_distance
7097
7198
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 ]:
80102 """
81103 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
87115 """
116+ validate_graph (graph_points )
117+
88118 n = len (graph_points )
119+ if n < 2 :
120+ raise InvalidGraphError ("Graph must have at least two nodes" )
121+
89122 nodes = list (graph_points .keys ())
90123
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 )]
93126 for i in range (n ):
94127 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+ )
96131
97- # dp[mask][i] represents the minimum distance to visit all nodes in the 'mask' set, ending at node i
98132 dp = [[float ("inf" )] * n for _ in range (1 << n )]
99- dp [1 ][0 ] = 0 # Start at node 0
133+ dp [1 ][0 ] = 0
100134
101- # Iterate over all subsets of nodes (represented by mask)
102135 for mask in range (1 << n ):
103136 for u in range (n ):
104137 if mask & (1 << u ):
@@ -109,18 +142,15 @@ def travelling_sales_man_problem_dp(graph_points):
109142 dp [next_mask ][v ], dp [mask ][u ] + dist [u ][v ]
110143 )
111144
112- # Reconstruct the path and find the minimum distance to return to the start
113145 final_mask = (1 << n ) - 1
114146 min_cost = float ("inf" )
115147 end_node = - 1
116148
117- # Find the minimum distance from any node back to the starting node
118149 for u in range (1 , n ):
119150 if min_cost > dp [final_mask ][u ] + dist [u ][0 ]:
120151 min_cost = dp [final_mask ][u ] + dist [u ][0 ]
121152 end_node = u
122153
123- # Reconstruct the path using the dp table
124154 path = []
125155 mask = final_mask
126156 while end_node != 0 :
@@ -142,14 +172,20 @@ def travelling_sales_man_problem_dp(graph_points):
142172
143173
144174if __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