Skip to content

New #12295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

New #12295

Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added TSP
  • Loading branch information
VarshiniShreeV committed Oct 27, 2024
commit e321b1e444c55c6059689dcfe6b17127b916c4ff
226 changes: 226 additions & 0 deletions travelling_salesman_problem.py
Original file line number Diff line number Diff line change
@@ -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}")