diff --git a/10-algo-ds-implementations/graph.py b/10-algo-ds-implementations/graph.py index 7d2e27f..3f4b1f0 100644 --- a/10-algo-ds-implementations/graph.py +++ b/10-algo-ds-implementations/graph.py @@ -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) \ No newline at end of file +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 \ No newline at end of file diff --git a/10-algo-ds-implementations/graph_fastest_path_dijkstra_array.py b/10-algo-ds-implementations/graph_fastest_path_dijkstra_array.py new file mode 100644 index 0000000..5134a70 --- /dev/null +++ b/10-algo-ds-implementations/graph_fastest_path_dijkstra_array.py @@ -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|) + + \ No newline at end of file diff --git a/10-algo-ds-implementations/graph_fastest_path_dijkstra_heapq.py b/10-algo-ds-implementations/graph_fastest_path_dijkstra_heapq.py new file mode 100644 index 0000000..f411002 --- /dev/null +++ b/10-algo-ds-implementations/graph_fastest_path_dijkstra_heapq.py @@ -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) + \ No newline at end of file