mirror of
https://github.com/TheAlgorithms/Python.git
synced 2025-04-06 22:05:54 +00:00
Added TSP
This commit is contained in:
parent
3ceccfb59c
commit
e321b1e444
226
travelling_salesman_problem.py
Normal file
226
travelling_salesman_problem.py
Normal file
@ -0,0 +1,226 @@
|
||||
""" 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}")
|
Loading…
x
Reference in New Issue
Block a user