diff --git a/pydatastructs/graphs/__init__.py b/pydatastructs/graphs/__init__.py index bdfebdc72..66d9e9acd 100644 --- a/pydatastructs/graphs/__init__.py +++ b/pydatastructs/graphs/__init__.py @@ -17,7 +17,8 @@ shortest_paths, all_pair_shortest_paths, topological_sort, - topological_sort_parallel + topological_sort_parallel, + lowest_common_ancestor ) __all__.extend(algorithms.__all__) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index cfb2b3330..2586ff513 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -11,6 +11,7 @@ from pydatastructs.graphs.graph import Graph from pydatastructs.linear_data_structures.algorithms import merge_sort_parallel from pydatastructs import PriorityQueue +import math __all__ = [ 'breadth_first_search', @@ -22,7 +23,8 @@ 'shortest_paths', 'all_pair_shortest_paths', 'topological_sort', - 'topological_sort_parallel' + 'topological_sort_parallel', + 'lowest_common_ancestor' ] Stack = Queue = deque @@ -993,3 +995,109 @@ def _job(graph: Graph, u: str): if len(L) != num_vertices: raise ValueError("Graph is not acyclic.") return L + +def lowest_common_ancestor(graph: Graph, vertex1: str, vertex2: str, algorithm: str) -> str: + """ + Finds the lowest common ancestor of two vertices u and v of a directed acylic graph. + The LCA of two vertices u and v is defined as the vertex w which is an ancestor + of both u and v and is farthest from the root vertex. + + Parameters + ========== + + graph: Graph + The graph under consideration. + vertex1, vertex2: str + The names of the vertices in the graph whose lowest common + ancestor is to be found. + algorithm: str + The algorithm to be used. + Currently, following are supported, + 'binary_lifting' -> Binary lifting algorithm as given in [1]. + + Returns + ======= + + str + The name of the vertex that is the lowest common ancestor of the two given + vertices in the given graph. + + Examples + ======== + + >>> from pydatastructs import Graph, AdjacencyListGraphNode, lowest_common_ancestor + >>> v_1 = AdjacencyListGraphNode('v_1') + >>> v_2 = AdjacencyListGraphNode('v_2') + >>> v_3 = AdjacencyListGraphNode('v_3') + >>> v_4 = AdjacencyListGraphNode('v_4') + >>> v_5 = AdjacencyListGraphNode('v_5') + >>> graph = Graph(v_1, v_2, v_3, v_4, v_5) + >>> graph.add_edge('v_1', 'v_2') + >>> graph.add_edge('v_1', 'v_3') + >>> graph.add_edge('v_3', 'v_4') + >>> graph.add_edge('v_3', 'v_5') + >>> lowest_common_ancestor(graph, 'v_5', 'v_2', 'binary_lifting') + 'v_1' + >>> lowest_common_ancestor(graph, 'v_4', 'v_5', 'binary_lifting') + 'v_3' + + References + ========== + + .. [1] https://www.geeksforgeeks.org/lca-in-a-tree-using-binary-lifting-technique/ + """ + import pydatastructs.graphs.algorithms as algorithms + func = "_" + algorithm + "_" + graph._impl + if not hasattr(algorithms, func): + raise NotImplementedError( + "Currently %s algorithm isn't implemented for " + "finding lowest common ancestor of two vertices in a %s graph." % (algorithm, graph._impl)) + return getattr(algorithms, func)(graph, vertex1, vertex2) + + +def _binary_lifting_adjacency_list(graph: Graph, vertex1: str, vertex2: str) -> list: + num_vertices = len(graph.vertices) + log_value = math.log2(num_vertices) + ancestor = {u: [""]*(int(log_value) + 1) for u in graph.vertices} + level = {u: 0 for u in graph.vertices} + def precompute(curr_node, next_node, ancestor, level): + if next_node != "" : + ancestor[next_node][0] = curr_node + level[next_node] = level[curr_node] + 1 + return True + + def _collect_source_nodes(graph: Graph) -> list: + S = [] + in_degree = {u: 0 for u in graph.vertices} + for u in graph.vertices: + for v in graph.neighbors(u): + in_degree[v.name] += 1 + for u in in_degree: + if in_degree[u] == 0: + S.append(u) + return list(S) + + source = _collect_source_nodes(graph)[0] + + depth_first_search(graph, source, precompute, ancestor, level) + for pow in range(1, int(log_value) + 1): + for vertex in graph.vertices: + if(ancestor[vertex][pow-1] != ""): + ancestor[vertex][pow] = ancestor[ancestor[vertex][pow - 1]][pow - 1] + + if level[vertex1] > level[vertex2]: + vertex1, vertex2 = vertex2, vertex1 + + difference = level[vertex2] - level[vertex1] + while(difference > 0): + pow_of_two =int(math.log2(difference)) + vertex2 = ancestor[vertex2][pow_of_two] + difference =- (1 << pow_of_two) + if vertex1 == vertex2: + return vertex1 + + for pow in range(int(log_value), -1, -1): + if ancestor[vertex1][pow] != "" and (ancestor[vertex1][pow] != ancestor[vertex2][pow]): + vertex1 = ancestor[vertex1][pow] + vertex2 = ancestor[vertex2][pow] + return ancestor[vertex1][0] diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index ddf274d1d..46543dafc 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -1,8 +1,11 @@ +from math import exp +from pydatastructs.linear_data_structures.algorithms import lower_bound +from pydatastructs.graphs.algorithms import lowest_common_ancestor from pydatastructs import (breadth_first_search, Graph, breadth_first_search_parallel, minimum_spanning_tree, minimum_spanning_tree_parallel, strongly_connected_components, depth_first_search, shortest_paths, topological_sort, -topological_sort_parallel) +topological_sort_parallel, lowest_common_ancestor) from pydatastructs.utils.raises_util import raises def test_breadth_first_search(): @@ -369,3 +372,56 @@ def _test_topological_sort(func, ds, algorithm, threads=None): _test_topological_sort(topological_sort, "List", "kahn") _test_topological_sort(topological_sort_parallel, "List", "kahn", 3) + +def test_lowest_common_ancestor(): + def _test_lowest_common_ancestor(ds): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + + V1 = GraphNode(0) + V2 = GraphNode(1) + V3 = GraphNode(2) + V4 = GraphNode(3) + V5 = GraphNode(4) + V6 = GraphNode(5) + V7 = GraphNode(6) + V8 = GraphNode(7) + + G1 = Graph(V1, V2, V3, V4, V5, V6, V7) + + edges = [ + (V1.name, V2.name), + (V1.name, V3.name), + (V3.name, V4.name), + (V3.name, V5.name), + (V5.name, V6.name), + (V5.name, V7.name), + (V2.name, V8.name) + ] + + for edge in edges: + G1.add_edge(*edge) + + lca = lowest_common_ancestor(G1, V6.name, V7.name) + expected_result = V5.name + assert(lca == expected_result) + + lca2 = lowest_common_ancestor(G1, V4.name, V7.name) + expected_result = V3.name + assert(lca2 == expected_result) + + lca3 = lowest_common_ancestor(G1, V7.name, V2.name) + expected_result = V1.name + assert(lca3 == expected_result) + + lca4 = lowest_common_ancestor(G1, V1.name, V6.name) + expected_result = V1.name + assert(lca4 == expected_result) + + lca5 = lowest_common_ancestor(G1, V3.name, V7.name) + expected_result = V3.name + assert(lca5 == expected_result) + + lca6 = lowest_common_ancestor(G1, V7.name, V8.name) + expected_result = V1.name + assert(lca6 == expected_result)