Skip to content

Commit 2a1028c

Browse files
author
Hamid
authored
Merge pull request #300 from hamidgasmi/297_dijkstra_algo
#297: implement dijkstra algorithm
2 parents ba76bda + 071f9a8 commit 2a1028c

File tree

3 files changed

+197
-8
lines changed

3 files changed

+197
-8
lines changed

10-algo-ds-implementations/graph.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
1+
from typing import List
2+
from collections import namedtuple
13

4+
'''
5+
Build Adjacency List
6+
Input: Eges List
7+
Output:
8+
- Graph implemented with an adjacency list
9+
10+
Time and Space Complexity:
11+
- Time Complexity: O(|E|)
12+
- Space Complexity: O(|V| + |E|)
13+
14+
'''
215

3-
class Weighted_Edge:
4-
def __init__(self, source: int, sink: int, weight: int):
5-
self.source = source
6-
self.sink = sink
7-
self.weight = weight
16+
Edge = namedtuple('Edge', ['source', 'sink', 'weight'])
817

9-
class Positively_Weighted_Edge(Weighted_Edge):
10-
def __init__(self, source: int, sink: int, weight: int):
11-
assert(weight >= 0)
18+
class Graph_Adjacency_List:
19+
def __init__(self, vertices_count: int, edges: List[Edge]):
20+
self.vertices_count = vertices_count
21+
self.adjacency_list = self.__build_adjacency_list(edges) # O(|E|)
22+
23+
def __build_adjacency_list(self, edges: List[Edge]) -> List[List[Edge]]:
24+
adjacency_list = [ [] for _ in range(self.__vertices_count) ]
25+
for edge in edges:
26+
adjacency_list[edge.source] = edge
27+
28+
return adjacency_list
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from typing import List
2+
from graph import Graph_Adjacency_List
3+
4+
'''
5+
Dijkstra's Algorithm
6+
Input: Weighted Graph: G(V, E, W), s ∈ V
7+
- Undirected or Directed
8+
- We >= 0 for all e in E
9+
10+
Output:
11+
- fastest path from s to any v in V
12+
- fastest path distances from s to any v in V
13+
14+
Time and Space Complexity:
15+
- Time Complexity: O(|V|^2)
16+
- T(Initialization) + T(Compute Min Distance) = O(|V|) + O(|V|^2 + |E|)
17+
- Dense graph (|E| = O(|V|^2)) --> T = O(|V|^2 + |V|^2) = O(|V|^2)
18+
- Sparce graph (|E| ~ |V|) --> T = O(|V|^2 + |V|) = O(|V|^2)
19+
- Space Complexity: O(|V|)
20+
- S(Parent list) + S(distance list) + S(visited nodes set) = O(|V| + |V| + |V|) = O(|V|)
21+
22+
'''
23+
24+
Candidate = namedtuple('Candidate', ['distance', 'node'])
25+
26+
class Dijkstra_Heap_Queue:
27+
def __init__(self, graph: Graph_Adjacency_List, s: int):
28+
self.__max_distance = 10**7
29+
30+
self.__graph = graph
31+
32+
self.__path_source_node = s
33+
self.__parent = []
34+
self.__distance = []
35+
36+
def fastest_distance_to(self, dest: int) -> int:
37+
if len(self.__distance) == 0:
38+
self.__compute_fastest_path()
39+
40+
return self.__distance[dest]
41+
42+
def fastest_path_to(self, dest: int) -> List[int]:
43+
if len(self.__distance) == 0:
44+
self.__compute_fastest_path()
45+
46+
v = dest
47+
reversed_path = []
48+
while v != -1:
49+
reversed_path.append(v)
50+
v = self.__parent[v]
51+
52+
return reversed_path[::-1]
53+
54+
# O(|V|)
55+
def __get_closest_node(self, visited_nodes: set) -> int:
56+
closest_node = -1
57+
min_distance = self.__max_distance
58+
for candidate in range(self.__graph.vertices_count):
59+
if candidate not in visited_nodes and self.__distance[candidate] < min_distance:
60+
closest_node = candidate
61+
min_distance = self.__distance[candidate]
62+
63+
return closest_node
64+
65+
def __compute_fastest_path (self):
66+
# Initialization
67+
self.__parent = [ -1 for _ in range(self.__graph.vertices_count) ] # O(|V|)
68+
self.__distance = [ self.__max_distance for _ in range(self.__graph.vertices_count) ] # O(|V|)
69+
70+
self.__distance[self.__path_source_node] = 0
71+
visted_nodes = set()
72+
73+
# Computing min distance for each v in V
74+
closest_node = 0
75+
while closest_node != -1: # |V| * T(self.__get_closest_node) + Sum(indegree(closest_node)) = |V|^2 + |E|
76+
distance = self.__distance[closest_node]
77+
visted_nodes.add(closest_node)
78+
79+
for edge in self.__graph.adjacency_list[closest_node]: # O(indegree(closest_node))
80+
adjacent = edge.sink
81+
candidate_distance = distance + edge.weight
82+
if candidate_distance < self.__distance[ adjacent ]:
83+
self.__parent[ adjacent ] = closest_node
84+
self.__distance[ adjacent ] = candidate_distance
85+
86+
closest_node = self.__get_closest_node(visted_nodes) # O(|V|)
87+
88+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import heapq
2+
from typing import List
3+
from graph import Graph_Adjacency_List
4+
5+
'''
6+
Dijkstra's Algorithm
7+
Input: Weighted Graph: G(V, E, W), s ∈ V
8+
- Undirected or Directed
9+
- We >= 0 for all e in E
10+
11+
Output:
12+
- fastest path from s to any v in V
13+
- fastest path distances from s to any v in V
14+
15+
Time and Space Complexity:
16+
- Time Complexity: O(|V| + |E|log|E|)
17+
- T(Initialization) + T(Compute Min Distance) = O(|V|) + P(|E|*log|E|)
18+
- Dense graph (|E| = O(|V|^2)) --> T = O(|V| + |V|^2 * log|V|^2) = O(|V|^2 * log|V|)
19+
- Sparce graph (|E| ~ |V|) --> T = O(|V| + |V|log|V|) = O(|V|log|V|)
20+
- Space Complexity: O(|V| + |E|)
21+
- S(Parent list) + S(distance list) + S(Heap queue) = O(|V| + |V| + |E|) = O(|V| + |E|)
22+
23+
Implementation Assumptions:
24+
- Directed graph
25+
- Edges number starts from 0 (zero-based numbering)
26+
- Edges list is valid
27+
- 0 <= edge.source, edge.sink < vertices_count for any edge in edges list
28+
- It does not have duplicates
29+
30+
'''
31+
32+
class Dijkstra_Heap_Queue:
33+
def __init__(self, graph: Graph_Adjacency_List, s: int):
34+
self.__max_distance = 10**7
35+
36+
self.__graph = graph
37+
38+
self.__path_source_node = s
39+
self.__parent = []
40+
self.__distance = []
41+
42+
def fastest_distance_to(self, dest: int) -> int:
43+
if len(self.__distance) == 0:
44+
self.__compute_fastest_path()
45+
46+
return self.__distance[dest]
47+
48+
def fastest_path_to(self, dest: int) -> List[int]:
49+
if len(self.__distance) == 0:
50+
self.__compute_fastest_path()
51+
52+
v = dest
53+
reversed_path = []
54+
while v != -1:
55+
reversed_path.append(v)
56+
v = self.__parent[v]
57+
58+
return reversed_path[::-1]
59+
60+
def __compute_fastest_path (self):
61+
# Initialization
62+
self.__parent = [ -1 for _ in range(self.__graph.vertices_count) ] # O(|V|)
63+
self.__distance = [ self.__max_distance for _ in range(self.__graph.vertices_count) ] # O(|V|)
64+
65+
self.__distance[self.__path_source_node] = 0
66+
closest_nodes_queue = [ (0, self.__path_source_node) ]
67+
68+
# Computing min distance for each v in V
69+
while closest_nodes_queue: # T(Push to hq) + T(Pop from hq) = O(2*|E|*log|E|) = O(|E|*log|E|):
70+
(distance, node) = heapq.heappop(closest_nodes_queue) # we can pop at most |E| edges from the hq
71+
72+
for edge in self.__graph.adjacency_list[node]:
73+
adjacent = edge.sink
74+
candidate_distance = distance + edge.weight
75+
if candidate_distance < self.__distance[ adjacent ]:
76+
self.__parent[ adjacent ] = node
77+
self.__distance[ adjacent ] = candidate_distance
78+
79+
# we can push at most |E| edges into the hq
80+
heapq.heappush(closest_nodes_queue, (candidate_distance, adjacent))
81+
# We could build a custom minheap that can return the position of an element in O(1).
82+
# We can then change its priority in O(log|V|): heapq.changepriority(closest_nodes_queue, adjacent, candidate_distance)
83+
# The heap queue will contain then at most: |V| elements (instead of |E| element as it's implemented here)
84+

0 commit comments

Comments
 (0)