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

Fixes #12128 #12147

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
193 changes: 193 additions & 0 deletions graphs/travlelling_salesman_problem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#!/usr/bin/env python3
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:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there is no test file in this pull request nor any test function or class in the file graphs/travlelling_salesman_problem.py, please provide doctest for the function validate_graph

"""
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
"""
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
):
raise InvalidGraphError("Each node must have a valid 2D coordinate [x, y]")


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)

>>> travelling_salesman_brute_force({})
Traceback (most recent call last):
...
travlelling_salesman_problem.InvalidGraphError: Graph must have at least two nodes
"""
validate_graph(graph_points)

nodes = list(graph_points.keys())
if len(nodes) < 2:
raise InvalidGraphError("Graph must have at least two nodes")

min_path = []
min_distance = float("inf")

start_node = nodes[0]
other_nodes = nodes[1:]

for perm in itertools.permutations(other_nodes):
path = [start_node, *list(perm), start_node]
total_distance = 0.0
for i in range(len(path) - 1):
total_distance += euclidean_distance(
graph_points[path[i]], graph_points[path[i + 1]]
)

if total_distance < min_distance:
min_distance = total_distance
min_path = path

return min_path, min_distance


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)
>>> travelling_salesman_dynamic_programming({})
Traceback (most recent call last):
...
travlelling_salesman_problem.InvalidGraphError: Graph must have at least two nodes
"""
validate_graph(graph_points)

n = len(graph_points)
if n < 2:
raise InvalidGraphError("Graph must have at least two nodes")

nodes = list(graph_points.keys())

# Initialize distance matrix with float values
dist = [[0.0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
dist[i][j] = euclidean_distance(
graph_points[nodes[i]], graph_points[nodes[j]]
)

dp = [[float("inf")] * n for _ in range(1 << n)]
dp[1][0] = 0

for mask in range(1 << n):
for u in range(n):
if mask & (1 << u):
for v in range(n):
if mask & (1 << v) == 0:
next_mask = mask | (1 << v)
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

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 (
mask & (1 << u)
and dp[mask][end_node]
== dp[mask ^ (1 << end_node)][u] + dist[u][end_node]
):
mask ^= 1 << end_node
end_node = u
break

path.append(nodes[0])
path.reverse()
path.append(nodes[0])

return path, min_cost


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],
}

# Example usage for brute force
brute_force_result = travelling_salesman_brute_force(demo_graph)
print(f"Brute force result: {brute_force_result}")

# Example usage for dynamic programming
dp_result = travelling_salesman_dynamic_programming(demo_graph)
print(f"Dynamic programming result: {dp_result}")
9 changes: 6 additions & 3 deletions greedy_methods/fractional_knapsack.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#!/usr/bin/env python3
from bisect import bisect
from itertools import accumulate

Expand Down Expand Up @@ -39,9 +40,11 @@ def frac_knapsack(vl, wt, w, n):
return (
0
if k == 0
else sum(vl[:k]) + (w - acc[k - 1]) * (vl[k]) / (wt[k])
if k != n
else sum(vl[:k])
else (
sum(vl[:k]) + (w - acc[k - 1]) * (vl[k]) / (wt[k])
if k != n
else sum(vl[:k])
)
)


Expand Down
10 changes: 6 additions & 4 deletions matrix/matrix_class.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# An OOP approach to representing and manipulating matrices

from __future__ import annotations


Expand Down Expand Up @@ -204,9 +204,11 @@ def cofactors(self) -> Matrix:
return Matrix(
[
[
self.minors().rows[row][column]
if (row + column) % 2 == 0
else self.minors().rows[row][column] * -1
(
self.minors().rows[row][column]
if (row + column) % 2 == 0
else self.minors().rows[row][column] * -1
)
for column in range(self.minors().num_columns)
]
for row in range(self.minors().num_rows)
Expand Down
Loading