From e321b1e444c55c6059689dcfe6b17127b916c4ff Mon Sep 17 00:00:00 2001
From: VarshiniShreeV <varshinishreevelumani@gmail.com>
Date: Sun, 27 Oct 2024 12:46:59 +0530
Subject: [PATCH] Added TSP

---
 travelling_salesman_problem.py | 226 +++++++++++++++++++++++++++++++++
 1 file changed, 226 insertions(+)
 create mode 100644 travelling_salesman_problem.py

diff --git a/travelling_salesman_problem.py b/travelling_salesman_problem.py
new file mode 100644
index 000000000..70f6cf637
--- /dev/null
+++ b/travelling_salesman_problem.py
@@ -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}")