@@ -20,27 +20,54 @@ class TSPEdge(Generic[T]):
20
20
weight : float
21
21
22
22
def __str__ (self ) -> str :
23
+ """
24
+ Examples:
25
+ >>> tsp_edge = TSPEdge.from_3_tuple(1, 2, 0.5)
26
+ >>> str(tsp_edge)
27
+ '(frozenset({1, 2}), 0.5)'
28
+ """
23
29
return f"({ self .vertices } , { self .weight } )"
24
30
25
- def __post_init__ (self ):
31
+ def __post_init__ (self ) -> None :
26
32
# Ensures that there is no loop in a vertex
27
33
if len (self .vertices ) != 2 :
28
34
raise ValueError ("frozenset must have exactly 2 elements" )
29
35
30
36
@classmethod
31
- def from_3_tuple (cls , x , y , w ) -> "TSPEdge" :
37
+ def from_3_tuple (cls , vertex_1 : T , vertex_2 : T , weight : float ) -> "TSPEdge" :
32
38
"""
33
39
Construct TSPEdge from a 3-tuple (x, y, w).
34
40
x & y are vertices and w is the weight.
41
+
42
+ Examples:
43
+ >>> tsp_edge = TSPEdge.from_3_tuple(1, 2, 0.5)
44
+ >>> tsp_edge.vertices
45
+ frozenset({1, 2})
46
+ >>> tsp_edge.weight
47
+ 0.5
35
48
"""
36
- return cls (frozenset ([x , y ]), w )
49
+ return cls (frozenset ([vertex_1 , vertex_2 ]), weight )
37
50
38
51
def __eq__ (self , other : object ) -> bool :
52
+ """
53
+ Examples:
54
+ >>> tsp_edge_1 = TSPEdge.from_3_tuple(1, 2, 0.5)
55
+ >>> tsp_edge_2 = TSPEdge.from_3_tuple(2, 1, 0.7)
56
+ >>> tsp_edge_1 == tsp_edge_2
57
+ True
58
+ """
39
59
if not isinstance (other , TSPEdge ):
40
60
return NotImplemented
41
61
return self .vertices == other .vertices
42
62
43
63
def __add__ (self , other : "TSPEdge" ) -> float :
64
+ """
65
+ Examples:
66
+ >>> tsp_edge_1 = TSPEdge.from_3_tuple(1, 2, 1.0)
67
+ >>> tsp_edge_2 = TSPEdge.from_3_tuple(2, 1, 2.5)
68
+ >>> tsp_edge_1 + tsp_edge_2
69
+ 3.5
70
+ """
44
71
return self .weight + other .weight
45
72
46
73
@@ -187,7 +214,7 @@ def adjacent_tuples(path: list[T]) -> zip:
187
214
Returns:
188
215
zip: A zip object containing tuples of adjacent vertices.
189
216
190
- Examples
217
+ Examples:
191
218
>>> list(adjacent_tuples([1, 2, 3, 4, 5]))
192
219
[(1, 2), (2, 3), (3, 4), (4, 5)]
193
220
@@ -209,6 +236,15 @@ def path_weight(path: list[T], tsp_graph: TSPGraph) -> float:
209
236
210
237
Returns:
211
238
float: The total weight of the path.
239
+
240
+ Examples:
241
+ >>> graph = TSPGraph.from_3_tuples((1, 2, 2), (2, 3, 4), (3, 4, 2), (4, 5, 1))
242
+ >>> path_weight([1, 2, 3], graph)
243
+ 6
244
+ >>> path_weight([1, 2, 3, 4], graph)
245
+ 8
246
+ >>> path_weight([1, 2, 3, 4, 5], graph)
247
+ 9
212
248
"""
213
249
return sum (tsp_graph .get_edge_weight (x , y ) for x , y in adjacent_tuples (path ))
214
250
@@ -228,6 +264,14 @@ def generate_paths(start: T, end: T, tsp_graph: TSPGraph) -> Generator[list[T]]:
228
264
229
265
Raises:
230
266
AssertionError: If start or end is not in the graph, or if they are the same.
267
+
268
+ Examples:
269
+ >>> graph = TSPGraph.from_3_tuples((1, 2, 2), (2, 3, 4), (3, 1, 2))
270
+ >>> graph_generator = generate_paths(1, 3, graph)
271
+ >>> next(graph_generator)
272
+ [1, 2, 3]
273
+ >>> next(graph_generator)
274
+ [1, 3]
231
275
"""
232
276
233
277
assert start in tsp_graph .vertices
@@ -257,7 +301,9 @@ def dfs(
257
301
yield from dfs (start , end , set (), [])
258
302
259
303
260
- def nearest_neighborhood (tsp_graph : TSPGraph , v , visited_ = None ) -> list [T ] | None :
304
+ def nearest_neighborhood (
305
+ tsp_graph : TSPGraph , current_vertex : T , visited_ : list [T ] | None = None
306
+ ) -> list [T ] | None :
261
307
"""
262
308
Approximates a solution to the Traveling Salesman Problem
263
309
using the Nearest Neighbor heuristic.
@@ -269,9 +315,29 @@ def nearest_neighborhood(tsp_graph: TSPGraph, v, visited_=None) -> list[T] | Non
269
315
270
316
Returns:
271
317
list[T] | None: A complete Hamiltonian cycle if possible, otherwise None.
318
+
319
+ Examples:
320
+ >>> edges = [
321
+ ... ("A", "B", 7), ("A", "D", 1), ("A", "E", 1),
322
+ ... ("B", "C", 3), ("B", "E", 8), ("C", "E", 2),
323
+ ... ("C", "D", 6), ("D", "E", 7)
324
+ ... ]
325
+ >>> graph = TSPGraph.from_3_tuples(*edges)
326
+ >>> import random
327
+ >>> init_v = random.choice(list(graph.vertices))
328
+ >>> result = nearest_neighborhood(graph, init_v)
329
+ >>> assert result in [
330
+ ... ['A', 'D', 'C', 'E', 'B', 'A'],
331
+ ... ['E', 'A', 'D', 'C', 'B', 'E'],
332
+ ... None
333
+ ... ]
334
+ >>> path_1 = ['A', 'D', 'C', 'E', 'B', 'A']
335
+ >>> path_2 = ['E', 'A', 'D', 'C', 'B', 'E']
336
+ >>> assert path_weight(path_1, graph) == 24 if result == path_1 else 19 or None
337
+ >>> assert path_weight(path_2, graph) == 19 if result == path_2 else 24 or None
272
338
"""
273
339
# Initialize visited list on first call
274
- visited = visited_ or [v ]
340
+ visited = visited_ or [current_vertex ]
275
341
276
342
# Base case: if all vertices are visited
277
343
if len (visited ) == len (tsp_graph .vertices ):
@@ -283,72 +349,23 @@ def nearest_neighborhood(tsp_graph: TSPGraph, v, visited_=None) -> list[T] | Non
283
349
284
350
# Get unvisited neighbors
285
351
filtered_neighbors = [
286
- tup for tup in tsp_graph .get_vertex_neighbor_weights (v ) if tup [0 ] not in visited
352
+ tup
353
+ for tup in tsp_graph .get_vertex_neighbor_weights (current_vertex )
354
+ if tup [0 ] not in visited
287
355
]
288
356
289
357
# If there are unvisited neighbors, continue to the nearest one
290
358
if filtered_neighbors :
291
359
next_v = min (filtered_neighbors , key = lambda tup : tup [1 ])[0 ]
292
- return nearest_neighborhood (tsp_graph , v = next_v , visited_ = [* visited , next_v ])
360
+ return nearest_neighborhood (
361
+ tsp_graph , current_vertex = next_v , visited_ = [* visited , next_v ]
362
+ )
293
363
else :
294
364
# No more neighbors, return None (cannot form a complete tour)
295
365
return None
296
366
297
367
298
- def sample_1 ():
299
- # Reference: https://graphicmaths.com/computer-science/graph-theory/travelling-salesman-problem/
300
-
301
- edges = [
302
- ("A" , "B" , 7 ),
303
- ("A" , "D" , 1 ),
304
- ("A" , "E" , 1 ),
305
- ("B" , "C" , 3 ),
306
- ("B" , "E" , 8 ),
307
- ("C" , "E" , 2 ),
308
- ("C" , "D" , 6 ),
309
- ("D" , "E" , 7 ),
310
- ]
311
-
312
- # Create the graph
313
- graph = TSPGraph .from_3_tuples (* edges )
314
-
315
- import random
316
-
317
- init_v = random .choice (list (graph .vertices ))
318
- optim_path = nearest_neighborhood (graph , init_v )
319
- # optim_path = nearest_neighborhood(graph, 'A')
320
- print (f"Optimal Cycle: { optim_path } " )
321
- if optim_path :
322
- print (f"Optimal Weight: { path_weight (optim_path , graph )} " )
323
-
324
-
325
- def sample_2 ():
326
- # Example 8x8 weight matrix (symmetric, no self-loops)
327
- weights = [
328
- [0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 ],
329
- [1 , 0 , 8 , 9 , 10 , 11 , 12 , 13 ],
330
- [2 , 8 , 0 , 14 , 15 , 16 , 17 , 18 ],
331
- [3 , 9 , 14 , 0 , 19 , 20 , 21 , 22 ],
332
- [4 , 10 , 15 , 19 , 0 , 23 , 24 , 25 ],
333
- [5 , 11 , 16 , 20 , 23 , 0 , 26 , 27 ],
334
- [6 , 12 , 17 , 21 , 24 , 26 , 0 , 28 ],
335
- [7 , 13 , 18 , 22 , 25 , 27 , 28 , 0 ],
336
- ]
337
-
338
- graph = TSPGraph .from_weights (weights )
339
-
340
- import random
341
-
342
- init_v = random .choice (list (graph .vertices ))
343
- optim_path = nearest_neighborhood (graph , init_v )
344
- print (f"Optimal Cycle: { optim_path } " )
345
- if optim_path :
346
- print (f"Optimal Weight: { path_weight (optim_path , graph )} " )
347
-
348
-
349
368
if __name__ == "__main__" :
350
369
import doctest
351
370
352
371
doctest .testmod ()
353
- sample_1 ()
354
- sample_2 ()
0 commit comments