From 13256b6f390e67e9dd6bd328087e2151f02ea4a2 Mon Sep 17 00:00:00 2001 From: Shivangi Nayak Date: Tue, 18 May 2021 04:08:27 +0530 Subject: [PATCH 1/4] lca algorithm and tests added --- pydatastructs/graphs/__init__.py | 3 +- pydatastructs/graphs/algorithms.py | 114 +++++++++++++++++- pydatastructs/graphs/tests/test_algorithms.py | 43 ++++++- 3 files changed, 156 insertions(+), 4 deletions(-) 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..f9e3212b1 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -11,7 +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', 'breadth_first_search_parallel', @@ -22,7 +22,8 @@ 'shortest_paths', 'all_pair_shortest_paths', 'topological_sort', - 'topological_sort_parallel' + 'topological_sort_parallel', + 'lowest_common_ancestor' ] Stack = Queue = deque @@ -993,3 +994,112 @@ 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_2', 'v_5', 'binary_lifting') + 'v_1' + >>> lowest_common_ancestor(graph, 'v_4', 'v_5', 'binary_lifting') + 'v_3' + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + """ + import pydatastructs.graphs.algorithms as algorithms + func = "_" + algorithm + "_" + graph._impl + if not hasattr(algorithms, func): + raise NotImplementedError( + "Currently %s algorithm isn't implemented for " + "performing topological sort on %s graphs." % (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} + parent= {} + def precompute(curr_node, next_node, ancestor, level, parent): + if next_node != "" : + ancestor[next_node][0] = curr_node + level[next_node] = level[curr_node] + 1 + parent[next_node] = curr_node + 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, parent) + for pow in range(1, int(log_value) + 1): + for vertex in graph.vertices: + if(ancestor[vertex][pow] != ""): + 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..2c27ee0c8 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -1,8 +1,10 @@ +from math import exp +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 +371,42 @@ 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) + + 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) + ] + + 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, V2.name, V7.name) + expected_result = V1.name + assert(lca3 == expected_result) From dbb43a84f8708430e629fcb5a17509007d7c04a6 Mon Sep 17 00:00:00 2001 From: Shivangi Nayak Date: Tue, 18 May 2021 23:11:05 +0530 Subject: [PATCH 2/4] removed reduntant variable --- pydatastructs/graphs/algorithms.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index f9e3212b1..5fa9fae68 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -12,6 +12,7 @@ from pydatastructs.linear_data_structures.algorithms import merge_sort_parallel from pydatastructs import PriorityQueue import math + __all__ = [ 'breadth_first_search', 'breadth_first_search_parallel', @@ -1059,12 +1060,10 @@ def _binary_lifting_adjacency_list(graph: Graph, vertex1: str, vertex2: str) -> 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} - parent= {} - def precompute(curr_node, next_node, ancestor, level, parent): + def precompute(curr_node, next_node, ancestor, level): if next_node != "" : ancestor[next_node][0] = curr_node level[next_node] = level[curr_node] + 1 - parent[next_node] = curr_node return True def _collect_source_nodes(graph: Graph) -> list: @@ -1080,7 +1079,7 @@ def _collect_source_nodes(graph: Graph) -> list: source = _collect_source_nodes(graph)[0] - depth_first_search(graph, source, precompute, ancestor, level, parent) + 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] != ""): From 9554c6679826f0c25017a7ed9955ad6faf608d79 Mon Sep 17 00:00:00 2001 From: Shivangi Nayak Date: Sun, 23 May 2021 23:11:29 +0530 Subject: [PATCH 3/4] added few tests and changed docstring --- pydatastructs/graphs/algorithms.py | 8 ++++---- pydatastructs/graphs/tests/test_algorithms.py | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 5fa9fae68..7b36e0350 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -1036,7 +1036,7 @@ def lowest_common_ancestor(graph: Graph, vertex1: str, vertex2: str, algorithm: >>> 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_2', 'v_5', 'binary_lifting') + >>> lowest_common_ancestor(graph, 'v_1', 'v_5', 'binary_lifting') 'v_1' >>> lowest_common_ancestor(graph, 'v_4', 'v_5', 'binary_lifting') 'v_3' @@ -1044,14 +1044,14 @@ def lowest_common_ancestor(graph: Graph, vertex1: str, vertex2: str, algorithm: References ========== - .. [1] https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm + .. [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 " - "performing topological sort on %s graphs." % (algorithm, graph._impl)) + "finding lowest common ancestor of two vertices in a %s graph." % (algorithm, graph._impl)) return getattr(algorithms, func)(graph, vertex1, vertex2) @@ -1082,7 +1082,7 @@ def _collect_source_nodes(graph: Graph) -> list: 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] != ""): + if(ancestor[vertex][pow-1] != ""): ancestor[vertex][pow] = ancestor[ancestor[vertex][pow - 1]][pow - 1] if level[vertex1] > level[vertex2]: diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index 2c27ee0c8..6864fee82 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -1,4 +1,5 @@ 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, @@ -410,3 +411,11 @@ def _test_lowest_common_ancestor(ds): lca3 = lowest_common_ancestor(G1, V2.name, V7.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) From 12461490d0b79748476ed28131d95d90479e432c Mon Sep 17 00:00:00 2001 From: Shivangi Nayak Date: Sun, 23 May 2021 23:56:53 +0530 Subject: [PATCH 4/4] modified tests --- pydatastructs/graphs/algorithms.py | 3 +-- pydatastructs/graphs/tests/test_algorithms.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 7b36e0350..2586ff513 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -1036,7 +1036,7 @@ def lowest_common_ancestor(graph: Graph, vertex1: str, vertex2: str, algorithm: >>> 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_1', 'v_5', 'binary_lifting') + >>> lowest_common_ancestor(graph, 'v_5', 'v_2', 'binary_lifting') 'v_1' >>> lowest_common_ancestor(graph, 'v_4', 'v_5', 'binary_lifting') 'v_3' @@ -1093,7 +1093,6 @@ def _collect_source_nodes(graph: Graph) -> list: pow_of_two =int(math.log2(difference)) vertex2 = ancestor[vertex2][pow_of_two] difference =- (1 << pow_of_two) - if vertex1 == vertex2: return vertex1 diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index 6864fee82..46543dafc 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -385,6 +385,7 @@ def _test_lowest_common_ancestor(ds): V5 = GraphNode(4) V6 = GraphNode(5) V7 = GraphNode(6) + V8 = GraphNode(7) G1 = Graph(V1, V2, V3, V4, V5, V6, V7) @@ -394,7 +395,8 @@ def _test_lowest_common_ancestor(ds): (V3.name, V4.name), (V3.name, V5.name), (V5.name, V6.name), - (V5.name, V7.name) + (V5.name, V7.name), + (V2.name, V8.name) ] for edge in edges: @@ -408,7 +410,7 @@ def _test_lowest_common_ancestor(ds): expected_result = V3.name assert(lca2 == expected_result) - lca3 = lowest_common_ancestor(G1, V2.name, V7.name) + lca3 = lowest_common_ancestor(G1, V7.name, V2.name) expected_result = V1.name assert(lca3 == expected_result) @@ -419,3 +421,7 @@ def _test_lowest_common_ancestor(ds): 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)