Skip to content

Commit f43009e

Browse files
committed
Dijkstra algorithm with notes (partially understand)
1 parent 48bdd5c commit f43009e

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

chap07_graphs/dijkstra.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Ref: https://dev.to/mxl/dijkstras-algorithm-in-python-algorithms-for-beginners-dkc
2+
3+
from collections import deque, namedtuple
4+
from math import inf
5+
6+
Edge = namedtuple(typename="Edge", field_names=["start", "end", "cost"])
7+
8+
9+
def make_edge(start, end, cost=1):
10+
return Edge(start, end, cost)
11+
12+
13+
class Graph:
14+
def __init__(self, edges):
15+
"""
16+
Validate & give tuples a name
17+
"""
18+
# Validate given tuples
19+
wrong_edges = [i for i in edges if len(i) not in (2, 3)]
20+
if wrong_edges:
21+
raise ValueError(f"Wrong edges data: {wrong_edges}")
22+
23+
# Returns a list of tuples (with a name called 'Edge')
24+
self.edges = [make_edge(*edge) for edge in edges]
25+
26+
@property
27+
def vertices(self):
28+
"""Return a set of strings that stands for vertices/nodes.
29+
"""
30+
return set(sum(([edge.start, edge.end] for edge in self.edges), []))
31+
32+
@property
33+
def neighbours(self):
34+
# Assign a default value (set()), i.e. { "a": set(), .. }
35+
neighbours = {vertex: set() for vertex in self.vertices}
36+
37+
# Assign a tuple (END, WEIGHT) to each vertices/nodes
38+
# what we're doing: loop << VERTEX[START].add((END, WEIGHT)) >>
39+
for edge in self.edges:
40+
neighbours[edge.start].add((edge.end, edge.cost))
41+
42+
return neighbours
43+
44+
def dijkstra(self, source, destination):
45+
"""
46+
During my step-by-step, I found that some of the steps(routes)
47+
might not be seemed clever or efficient, BUT, the final result
48+
still achieves "the shortest route", amazing! (well, I think it's
49+
mainly due to I havn't really fully understand this algorithm XD)
50+
"""
51+
52+
# Validaton on whether your `source` is in the nodes
53+
assert source in self.vertices, "Such source node does not exist"
54+
55+
# Assign a default value (Infinity), i.e. { "a": Infinity, .. }
56+
distances = {vertex: inf for vertex in self.vertices}
57+
58+
# Assign a default value (None), i.e. { "a": None, .. }
59+
previous_vertices = {vertex: None for vertex in self.vertices}
60+
61+
# Assign 0 to the source node (therefore became the smallest)
62+
distances[source] = 0
63+
64+
# A copy of set of strings for calc the cost (del one after each loop)
65+
vertices = self.vertices.copy()
66+
67+
# This whole algorithm takes about 779 steps, 553 of them in this loop.
68+
while vertices:
69+
# Each round the loop would do these things:
70+
# 1) record the length of the route
71+
# 2) store the map between START to NEXT_DEST
72+
# 3) finally, remove the key after done two things above
73+
74+
# find the vertex (=> a string) holds the smallest distance
75+
current_vertex = min(
76+
vertices, key=lambda vertex: distances[vertex]
77+
)
78+
79+
# Ignore if vertex is inf (== "is in initial state")
80+
if distances[current_vertex] == inf:
81+
break
82+
83+
# for { NEXT_DEST, WEIGHT } in a set of { NODE: set(X, Y) }
84+
for neighbour, cost in self.neighbours[current_vertex]:
85+
# form a route (0 + next_dest_cost) from START to NEIGHBOUR
86+
# returns a integer (sum of cost (from here to there))
87+
alternative_route = distances[current_vertex] + cost
88+
89+
# real route VERSUS default inf
90+
# -> update distance[NEXT_DEST] = length of real route
91+
# -> set_of_nodes_with_None[NEXT_DEST] = "a" (like tracing)
92+
if alternative_route < distances[neighbour]:
93+
distances[neighbour] = alternative_route
94+
previous_vertices[neighbour] = current_vertex
95+
96+
# remove the string(vertex|node) from the set
97+
vertices.remove(current_vertex)
98+
99+
path, current_vertex = deque(), destination
100+
while previous_vertices[current_vertex] is not None:
101+
path.appendleft(current_vertex)
102+
current_vertex = previous_vertices[current_vertex]
103+
104+
if path:
105+
path.appendleft(current_vertex)
106+
107+
return path
108+
109+
def get_node_pairs(self, node1, node2, both_ends=True):
110+
if both_ends is True:
111+
node_pairs = [[node1, node2], [node2, node1]]
112+
else:
113+
node_pairs = [[node1, node2]]
114+
115+
return node_pairs
116+
117+
def add_edge(self, node1, node2, cost=1, both_ends=True):
118+
node_pairs = self.get_node_pairs(
119+
node1=node1, node2=node2, both_ends=True
120+
)
121+
for edge in self.edges:
122+
if [edge.start, edge.end] in node_pairs:
123+
raise ValueError(f"Edge {node1} {node2} already exists")
124+
125+
self.edges.append(Edge(start=node1, end=node2, cost=cost))
126+
if both_ends:
127+
self.edges.append(Edge(start=node2, end=node1, cost=cost))
128+
129+
def remove_edge(self, node1, node2, both_ends=True):
130+
node_pairs = self.get_node_pairs(
131+
node1=node1, node2=node2, both_ends=True
132+
)
133+
edges = self.edges[:]
134+
for edge in edges:
135+
if [edge.start, edge.end] in node_pairs:
136+
self.edges.remove(edge)
137+
138+
139+
def main() -> None:
140+
graph = Graph(
141+
[
142+
("a", "b", 7),
143+
("a", "c", 9),
144+
("a", "f", 14),
145+
("b", "c", 10),
146+
("b", "d", 15),
147+
("c", "d", 11),
148+
("c", "f", 2),
149+
("d", "e", 6),
150+
("e", "f", 9),
151+
]
152+
)
153+
154+
assert graph.dijkstra("a", "e") == deque(["a", "c", "d", "e"])
155+
156+
157+
if "__main__" == __name__:
158+
main()

0 commit comments

Comments
 (0)