Skip to content

#297: implement dijkstra algorithm #300

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

Merged
merged 10 commits into from
Jul 2, 2022
Merged
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
33 changes: 25 additions & 8 deletions 10-algo-ds-implementations/graph.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
from typing import List
from collections import namedtuple

'''
Build Adjacency List
Input: Eges List
Output:
- Graph implemented with an adjacency list

Time and Space Complexity:
- Time Complexity: O(|E|)
- Space Complexity: O(|V| + |E|)

'''

class Weighted_Edge:
def __init__(self, source: int, sink: int, weight: int):
self.source = source
self.sink = sink
self.weight = weight
Edge = namedtuple('Edge', ['source', 'sink', 'weight'])

class Positively_Weighted_Edge(Weighted_Edge):
def __init__(self, source: int, sink: int, weight: int):
assert(weight >= 0)
class Graph_Adjacency_List:
def __init__(self, vertices_count: int, edges: List[Edge]):
self.vertices_count = vertices_count
self.adjacency_list = self.__build_adjacency_list(edges) # O(|E|)

def __build_adjacency_list(self, edges: List[Edge]) -> List[List[Edge]]:
adjacency_list = [ [] for _ in range(self.__vertices_count) ]
for edge in edges:
adjacency_list[edge.source] = edge

return adjacency_list
88 changes: 88 additions & 0 deletions 10-algo-ds-implementations/graph_fastest_path_dijkstra_array.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from typing import List
from graph import Graph_Adjacency_List

'''
Dijkstra's Algorithm
Input: Weighted Graph: G(V, E, W), s ∈ V
- Undirected or Directed
- We >= 0 for all e in E

Output:
- fastest path from s to any v in V
- fastest path distances from s to any v in V

Time and Space Complexity:
- Time Complexity: O(|V|^2)
- T(Initialization) + T(Compute Min Distance) = O(|V|) + O(|V|^2 + |E|)
- Dense graph (|E| = O(|V|^2)) --> T = O(|V|^2 + |V|^2) = O(|V|^2)
- Sparce graph (|E| ~ |V|) --> T = O(|V|^2 + |V|) = O(|V|^2)
- Space Complexity: O(|V|)
- S(Parent list) + S(distance list) + S(visited nodes set) = O(|V| + |V| + |V|) = O(|V|)

'''

Candidate = namedtuple('Candidate', ['distance', 'node'])

class Dijkstra_Heap_Queue:
def __init__(self, graph: Graph_Adjacency_List, s: int):
self.__max_distance = 10**7

self.__graph = graph

self.__path_source_node = s
self.__parent = []
self.__distance = []

def fastest_distance_to(self, dest: int) -> int:
if len(self.__distance) == 0:
self.__compute_fastest_path()

return self.__distance[dest]

def fastest_path_to(self, dest: int) -> List[int]:
if len(self.__distance) == 0:
self.__compute_fastest_path()

v = dest
reversed_path = []
while v != -1:
reversed_path.append(v)
v = self.__parent[v]

return reversed_path[::-1]

# O(|V|)
def __get_closest_node(self, visited_nodes: set) -> int:
closest_node = -1
min_distance = self.__max_distance
for candidate in range(self.__graph.vertices_count):
if candidate not in visited_nodes and self.__distance[candidate] < min_distance:
closest_node = candidate
min_distance = self.__distance[candidate]

return closest_node

def __compute_fastest_path (self):
# Initialization
self.__parent = [ -1 for _ in range(self.__graph.vertices_count) ] # O(|V|)
self.__distance = [ self.__max_distance for _ in range(self.__graph.vertices_count) ] # O(|V|)

self.__distance[self.__path_source_node] = 0
visted_nodes = set()

# Computing min distance for each v in V
closest_node = 0
while closest_node != -1: # |V| * T(self.__get_closest_node) + Sum(indegree(closest_node)) = |V|^2 + |E|
distance = self.__distance[closest_node]
visted_nodes.add(closest_node)

for edge in self.__graph.adjacency_list[closest_node]: # O(indegree(closest_node))
adjacent = edge.sink
candidate_distance = distance + edge.weight
if candidate_distance < self.__distance[ adjacent ]:
self.__parent[ adjacent ] = closest_node
self.__distance[ adjacent ] = candidate_distance

closest_node = self.__get_closest_node(visted_nodes) # O(|V|)


84 changes: 84 additions & 0 deletions 10-algo-ds-implementations/graph_fastest_path_dijkstra_heapq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import heapq
from typing import List
from graph import Graph_Adjacency_List

'''
Dijkstra's Algorithm
Input: Weighted Graph: G(V, E, W), s ∈ V
- Undirected or Directed
- We >= 0 for all e in E

Output:
- fastest path from s to any v in V
- fastest path distances from s to any v in V

Time and Space Complexity:
- Time Complexity: O(|V| + |E|log|E|)
- T(Initialization) + T(Compute Min Distance) = O(|V|) + P(|E|*log|E|)
- Dense graph (|E| = O(|V|^2)) --> T = O(|V| + |V|^2 * log|V|^2) = O(|V|^2 * log|V|)
- Sparce graph (|E| ~ |V|) --> T = O(|V| + |V|log|V|) = O(|V|log|V|)
- Space Complexity: O(|V| + |E|)
- S(Parent list) + S(distance list) + S(Heap queue) = O(|V| + |V| + |E|) = O(|V| + |E|)

Implementation Assumptions:
- Directed graph
- Edges number starts from 0 (zero-based numbering)
- Edges list is valid
- 0 <= edge.source, edge.sink < vertices_count for any edge in edges list
- It does not have duplicates

'''

class Dijkstra_Heap_Queue:
def __init__(self, graph: Graph_Adjacency_List, s: int):
self.__max_distance = 10**7

self.__graph = graph

self.__path_source_node = s
self.__parent = []
self.__distance = []

def fastest_distance_to(self, dest: int) -> int:
if len(self.__distance) == 0:
self.__compute_fastest_path()

return self.__distance[dest]

def fastest_path_to(self, dest: int) -> List[int]:
if len(self.__distance) == 0:
self.__compute_fastest_path()

v = dest
reversed_path = []
while v != -1:
reversed_path.append(v)
v = self.__parent[v]

return reversed_path[::-1]

def __compute_fastest_path (self):
# Initialization
self.__parent = [ -1 for _ in range(self.__graph.vertices_count) ] # O(|V|)
self.__distance = [ self.__max_distance for _ in range(self.__graph.vertices_count) ] # O(|V|)

self.__distance[self.__path_source_node] = 0
closest_nodes_queue = [ (0, self.__path_source_node) ]

# Computing min distance for each v in V
while closest_nodes_queue: # T(Push to hq) + T(Pop from hq) = O(2*|E|*log|E|) = O(|E|*log|E|):
(distance, node) = heapq.heappop(closest_nodes_queue) # we can pop at most |E| edges from the hq

for edge in self.__graph.adjacency_list[node]:
adjacent = edge.sink
candidate_distance = distance + edge.weight
if candidate_distance < self.__distance[ adjacent ]:
self.__parent[ adjacent ] = node
self.__distance[ adjacent ] = candidate_distance

# we can push at most |E| edges into the hq
heapq.heappush(closest_nodes_queue, (candidate_distance, adjacent))
# We could build a custom minheap that can return the position of an element in O(1).
# We can then change its priority in O(log|V|): heapq.changepriority(closest_nodes_queue, adjacent, candidate_distance)
# The heap queue will contain then at most: |V| elements (instead of |E| element as it's implemented here)