Skip to content

Commit 9424ad8

Browse files
Merge branch 'master' into add-clear-least-significant-set-bit
2 parents df94c42 + 14c971f commit 9424ad8

3 files changed

Lines changed: 270 additions & 11 deletions

File tree

DIRECTORY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
* [Intensity Based Segmentation](computer_vision/intensity_based_segmentation.py)
188188
* [Mean Threshold](computer_vision/mean_threshold.py)
189189
* [Mosaic Augmentation](computer_vision/mosaic_augmentation.py)
190+
* [Otsu Threshold](computer_vision/otsu_threshold.py)
190191
* [Pooling Functions](computer_vision/pooling_functions.py)
191192
* [Vision Transformer](computer_vision/vision_transformer.py)
192193

@@ -454,6 +455,7 @@
454455
* [Minimum Steps To One](dynamic_programming/minimum_steps_to_one.py)
455456
* [Minimum Tickets Cost](dynamic_programming/minimum_tickets_cost.py)
456457
* [Narcissistic Number](dynamic_programming/narcissistic_number.py)
458+
* [Needleman Wunsch](dynamic_programming/needleman_wunsch.py)
457459
* [Optimal Binary Search Tree](dynamic_programming/optimal_binary_search_tree.py)
458460
* [Palindrome Partitioning](dynamic_programming/palindrome_partitioning.py)
459461
* [Range Sum Query](dynamic_programming/range_sum_query.py)
@@ -613,6 +615,7 @@
613615
* [Test Johnson](graphs/tests/test_johnson.py)
614616
* [Test Min Spanning Tree Kruskal](graphs/tests/test_min_spanning_tree_kruskal.py)
615617
* [Test Min Spanning Tree Prim](graphs/tests/test_min_spanning_tree_prim.py)
618+
* [Travelling Salesman Problem](graphs/travelling_salesman_problem.py)
616619

617620
## [Greedy Methods](greedy_methods)
618621
* [Best Time To Buy And Sell Stock](greedy_methods/best_time_to_buy_and_sell_stock.py)
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""Travelling Salesman Problem (TSP)"""
2+
3+
import itertools
4+
import math
5+
6+
7+
class InvalidGraphError(ValueError):
8+
"""Custom error for invalid graph inputs."""
9+
10+
11+
def euclidean_distance(point1: list[float], point2: list[float]) -> float:
12+
"""
13+
Calculate the Euclidean distance between two points in 2D space.
14+
15+
:param point1: Coordinates of the first point [x, y]
16+
:param point2: Coordinates of the second point [x, y]
17+
:return: The Euclidean distance between the two points
18+
19+
>>> euclidean_distance([0, 0], [3, 4])
20+
5.0
21+
>>> euclidean_distance([1, 1], [1, 1])
22+
0.0
23+
>>> euclidean_distance([1, 1], ['a', 1])
24+
Traceback (most recent call last):
25+
...
26+
ValueError: Invalid input: Points must be numerical coordinates
27+
"""
28+
try:
29+
return math.sqrt((point2[0] - point1[0]) ** 2 + (point2[1] - point1[1]) ** 2)
30+
except TypeError:
31+
raise ValueError("Invalid input: Points must be numerical coordinates")
32+
33+
34+
def validate_graph(graph_points: dict[str, list[float]]) -> None:
35+
"""
36+
Validate the input graph to ensure it has valid nodes and coordinates.
37+
38+
:param graph_points: A dictionary where the keys are node names,
39+
and values are 2D coordinates as [x, y]
40+
:raises InvalidGraphError: If the graph points are not valid
41+
42+
>>> validate_graph({"A": [10, 20], "B": [30, 21], "C": [15, 35]}) # Valid graph
43+
>>> validate_graph( # doctest: +IGNORE_EXCEPTION_DETAIL
44+
... {"A": [10, 20], "B": [30, "invalid"], "C": [15, 35]}
45+
... )
46+
Traceback (most recent call last):
47+
...
48+
InvalidGraphError: Each node must have a valid 2D coordinate [x, y]
49+
50+
>>> validate_graph([10, 20]) # doctest: +IGNORE_EXCEPTION_DETAIL
51+
Traceback (most recent call last):
52+
...
53+
InvalidGraphError: Graph must be a dictionary with node names and coordinates
54+
55+
>>> validate_graph( # doctest: +IGNORE_EXCEPTION_DETAIL
56+
... {"A": [10, 20], "B": [30, 21], "C": [15]}
57+
... ) # Missing coordinate
58+
Traceback (most recent call last):
59+
...
60+
InvalidGraphError: Each node must have a valid 2D coordinate [x, y]
61+
"""
62+
if not isinstance(graph_points, dict):
63+
raise InvalidGraphError(
64+
"Graph must be a dictionary with node names and coordinates"
65+
)
66+
67+
for node, coordinates in graph_points.items():
68+
if (
69+
not isinstance(node, str)
70+
or not isinstance(coordinates, list)
71+
or len(coordinates) != 2
72+
or not all(isinstance(c, (int, float)) for c in coordinates)
73+
):
74+
raise InvalidGraphError("Each node must have a valid 2D coordinate [x, y]")
75+
76+
77+
# TSP in Brute Force Approach
78+
def travelling_salesman_brute_force(
79+
graph_points: dict[str, list[float]],
80+
) -> tuple[list[str], float]:
81+
"""
82+
Solve the Travelling Salesman Problem using brute force.
83+
84+
:param graph_points: A dictionary of nodes and their coordinates {node: [x, y]}
85+
:return: The shortest path and its total distance
86+
87+
>>> graph = {"A": [10, 20], "B": [30, 21], "C": [15, 35]}
88+
>>> travelling_salesman_brute_force(graph)
89+
(['A', 'B', 'C', 'A'], 56.35465722402588)
90+
"""
91+
validate_graph(graph_points)
92+
93+
nodes = list(graph_points.keys()) # Extracting the node names (keys)
94+
95+
# There should be at least 2 nodes for a valid TSP
96+
if len(nodes) < 2:
97+
raise InvalidGraphError("Graph must have at least two nodes")
98+
99+
min_path = [] # List that stores shortest path
100+
min_distance = float("inf") # Initialize minimum distance to infinity
101+
102+
start_node = nodes[0]
103+
other_nodes = nodes[1:]
104+
105+
# Iterating over all permutations of the other nodes
106+
for perm in itertools.permutations(other_nodes):
107+
path = [start_node, *perm, start_node]
108+
109+
# Calculating the total distance
110+
total_distance = sum(
111+
euclidean_distance(graph_points[path[i]], graph_points[path[i + 1]])
112+
for i in range(len(path) - 1)
113+
)
114+
115+
# Update minimum distance if shorter path found
116+
if total_distance < min_distance:
117+
min_distance = total_distance
118+
min_path = path
119+
120+
return min_path, min_distance
121+
122+
123+
# TSP in Dynamic Programming approach
124+
def travelling_salesman_dynamic_programming(
125+
graph_points: dict[str, list[float]],
126+
) -> tuple[list[str], float]:
127+
"""
128+
Solve the Travelling Salesman Problem using dynamic programming.
129+
130+
:param graph_points: A dictionary of nodes and their coordinates {node: [x, y]}
131+
:return: The shortest path and its total distance
132+
133+
>>> graph = {"A": [10, 20], "B": [30, 21], "C": [15, 35]}
134+
>>> travelling_salesman_dynamic_programming(graph)
135+
(['A', 'C', 'B', 'A'], 56.35465722402587)
136+
"""
137+
validate_graph(graph_points)
138+
139+
n = len(graph_points) # Extracting the node names (keys)
140+
141+
# There should be at least 2 nodes for a valid TSP
142+
if n < 2:
143+
raise InvalidGraphError("Graph must have at least two nodes")
144+
145+
nodes = list(graph_points.keys()) # Extracting the node names (keys)
146+
147+
# Initialize distance matrix with float values
148+
dist = [
149+
[
150+
euclidean_distance(graph_points[nodes[i]], graph_points[nodes[j]])
151+
for j in range(n)
152+
]
153+
for i in range(n)
154+
]
155+
156+
# Initialize a dynamic programming table with infinity
157+
dp = [[float("inf")] * n for _ in range(1 << n)]
158+
dp[1][0] = 0 # Only visited node is the starting point at node 0
159+
160+
# Iterate through all masks of visited nodes
161+
for mask in range(1 << n):
162+
for u in range(n):
163+
# If current node 'u' is visited
164+
if mask & (1 << u):
165+
# Traverse nodes 'v' such that u->v
166+
for v in range(n):
167+
if mask & (1 << v) == 0: # If v is not visited
168+
next_mask = mask | (1 << v) # Upodate mask to include 'v'
169+
# Update dynamic programming table with minimum distance
170+
dp[next_mask][v] = min(
171+
dp[next_mask][v], dp[mask][u] + dist[u][v]
172+
)
173+
174+
final_mask = (1 << n) - 1
175+
min_cost = float("inf")
176+
end_node = -1 # Track the last node in the optimal path
177+
178+
for u in range(1, n):
179+
if min_cost > dp[final_mask][u] + dist[u][0]:
180+
min_cost = dp[final_mask][u] + dist[u][0]
181+
end_node = u
182+
183+
path = []
184+
mask = final_mask
185+
while end_node != 0:
186+
path.append(nodes[end_node])
187+
for u in range(n):
188+
# If current state corresponds to optimal state before visiting end node
189+
if (
190+
mask & (1 << u)
191+
and dp[mask][end_node]
192+
== dp[mask ^ (1 << end_node)][u] + dist[u][end_node]
193+
):
194+
mask ^= 1 << end_node # Update mask to remove end node
195+
end_node = u # Set the previous node as end node
196+
break
197+
198+
path.append(nodes[0]) # Bottom-up Order
199+
path.reverse() # Top-Down Order
200+
path.append(nodes[0])
201+
202+
return path, min_cost
203+
204+
205+
# Demo Graph
206+
# C (15, 35)
207+
# |
208+
# |
209+
# |
210+
# F (5, 15) --- A (10, 20)
211+
# | |
212+
# | |
213+
# | |
214+
# | |
215+
# E (25, 5) --- B (30, 21)
216+
# |
217+
# |
218+
# |
219+
# D (40, 10)
220+
# |
221+
# |
222+
# |
223+
# G (50, 25)
224+
225+
226+
if __name__ == "__main__":
227+
demo_graph = {
228+
"A": [10.0, 20.0],
229+
"B": [30.0, 21.0],
230+
"C": [15.0, 35.0],
231+
"D": [40.0, 10.0],
232+
"E": [25.0, 5.0],
233+
"F": [5.0, 15.0],
234+
"G": [50.0, 25.0],
235+
}
236+
237+
# Brute force
238+
brute_force_result = travelling_salesman_brute_force(demo_graph)
239+
print(f"Brute force result: {brute_force_result}")
240+
241+
# Dynamic programming
242+
dp_result = travelling_salesman_dynamic_programming(demo_graph)
243+
print(f"Dynamic programming result: {dp_result}")

sorts/topological_sort.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Topological Sort.
1+
"""Topological Sort on Directed Acyclic Graph(DAG)
22
33
https://en.wikipedia.org/wiki/Topological_sorting
44
https://en.wikipedia.org/wiki/Directed_acyclic_graph
@@ -9,19 +9,22 @@
99

1010
# a
1111
# / \
12-
# b c
12+
# b c
1313
# / \
14-
# d e
14+
# d e
15+
1516
edges: dict[str, list[str]] = {
1617
"a": ["c", "b"],
1718
"b": ["d", "e"],
1819
"c": [],
1920
"d": [],
2021
"e": [],
2122
}
23+
2224
vertices: list[str] = ["a", "b", "c", "d", "e"]
2325

2426

27+
# Perform topological sort on a DAG starting from the specified node
2528
def topological_sort(start: str, visited: list[str], sort: list[str]) -> list[str]:
2629
"""
2730
Perform topological sort on a directed acyclic graph.
@@ -44,24 +47,34 @@ def topological_sort(start: str, visited: list[str], sort: list[str]) -> list[st
4447
if not isinstance(sort, list):
4548
raise ValueError("sort must be a list")
4649
current = start
47-
# add current to visited
50+
# Mark the current node as visited
4851
visited.append(current)
52+
# List of all neighbors of current node
4953
neighbors = edges[current]
54+
55+
# Traverse all neighbors of the current node
5056
for neighbor in neighbors:
51-
# if neighbor not in visited, visit
57+
# Recursively visit each unvisited neighbor
5258
if neighbor not in visited:
5359
sort = topological_sort(neighbor, visited, sort)
54-
# if all neighbors visited add current to sort
60+
61+
# After visiting all neighbors, add the current node to the sorted list
5562
sort.append(current)
56-
# if all vertices haven't been visited select a new one to visit
63+
64+
# If there are some nodes that were not visited (disconnected components)
5765
if len(visited) != len(vertices):
58-
for vertice in vertices:
59-
if vertice not in visited:
60-
sort = topological_sort(vertice, visited, sort)
61-
# return sort
66+
for vertex in vertices:
67+
if vertex not in visited:
68+
sort = topological_sort(vertex, visited, sort)
69+
70+
# Return sorted list
6271
return sort
6372

6473

6574
if __name__ == "__main__":
75+
# Topological Sorting from node "a" (Returns the order in bottom up approach)
6676
sort = topological_sort("a", [], [])
77+
78+
# Reversing the list to get the correct topological order (Top down approach)
79+
sort.reverse()
6780
print(sort)

0 commit comments

Comments
 (0)