|
| 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