Skip to content
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

New #12295

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

New #12295

Show file tree
Hide file tree
Changes from all commits
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
36 changes: 24 additions & 12 deletions sorts/topological_sort.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,53 @@
"""Topological Sort."""
"""Topological Sort on Directed Acyclic Graph(DAG)"""

# a
# / \
# b c
# b c
# / \
# d e
# d e

edges: dict[str, list[str]] = {
"a": ["c", "b"],
"b": ["d", "e"],
"c": [],
"d": [],
"e": [],
}

vertices: list[str] = ["a", "b", "c", "d", "e"]


# Perform topological sort on a DAG starting from the specified node
def topological_sort(start: str, visited: list[str], sort: list[str]) -> list[str]:
"""Perform topological sort on a directed acyclic graph."""
current = start
# add current to visited
# Mark the current node as visited
visited.append(current)
# List of all neighbors of current node
neighbors = edges[current]

# Traverse all neighbors of the current node
for neighbor in neighbors:
# if neighbor not in visited, visit
# Recursively visit each unvisited neighbor
if neighbor not in visited:
sort = topological_sort(neighbor, visited, sort)
# if all neighbors visited add current to sort

# After visiting all neighbors, add the current node to the sorted list
sort.append(current)
# if all vertices haven't been visited select a new one to visit

# If there are some nodes that were not visited (disconnected components)
if len(visited) != len(vertices):
for vertice in vertices:
if vertice not in visited:
sort = topological_sort(vertice, visited, sort)
# return sort
for vertex in vertices:
if vertex not in visited:
sort = topological_sort(vertex, visited, sort)

# Return sorted list
return sort


if __name__ == "__main__":
# Topological Sorting from node "a" (Returns the order in bottom up approach)
sort = topological_sort("a", [], [])

# Reversing the list to get the correct topological order (Top down approach)
sort.reverse()
print(sort)
239 changes: 239 additions & 0 deletions travelling_salesman_problem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"""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