diff --git a/.travis.yml b/.travis.yml index 70dc76969..3cdaff76f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ sudo: false language: python python: - - "3.5" + - "3.6" install: - pip install -r requirements.txt diff --git a/pydatastructs/graphs/__init__.py b/pydatastructs/graphs/__init__.py index 6c2f3ba90..fb8a518db 100644 --- a/pydatastructs/graphs/__init__.py +++ b/pydatastructs/graphs/__init__.py @@ -9,7 +9,8 @@ from . import algorithms from .algorithms import ( breadth_first_search, - breadth_first_search_parallel + breadth_first_search_parallel, + minimum_spanning_tree ) __all__.extend(algorithms.__all__) diff --git a/pydatastructs/graphs/adjacency_list.py b/pydatastructs/graphs/adjacency_list.py index 9adc6a383..d596633db 100644 --- a/pydatastructs/graphs/adjacency_list.py +++ b/pydatastructs/graphs/adjacency_list.py @@ -1,5 +1,6 @@ from pydatastructs.graphs.graph import Graph from pydatastructs.linear_data_structures import DynamicOneDimensionalArray +from pydatastructs.utils.misc_util import GraphEdge __all__ = [ 'AdjacencyList' @@ -19,6 +20,7 @@ def __new__(cls, *vertices): for vertex in vertices: obj.__setattr__(vertex.name, vertex) obj.vertices = set([vertex.name for vertex in vertices]) + obj.edge_weights = dict() return obj def is_adjacent(self, node1, node2): @@ -42,14 +44,24 @@ def remove_vertex(self, name): delattr(node_obj, name) node_obj.adjacent.remove(name) - def add_edge(self, source, target): + def add_edge(self, source, target, cost=None): source, target = self.__getattribute__(source), \ self.__getattribute__(target) source.__setattr__(target.name, target) source.adjacent.add(target.name) + if cost is not None: + self.edge_weights[source.name + "_" + target.name] = \ + GraphEdge(source, target, cost) + + def get_edge(self, source, target): + return self.edge_weights.get( + source + "_" + target, + None) def remove_edge(self, source, target): source, target = self.__getattribute__(source), \ self.__getattribute__(target) source.adjacent.remove(target.name) delattr(source, target.name) + self.edge_weights.pop(source.name + "_" + target.name, + None) diff --git a/pydatastructs/graphs/adjacency_matrix.py b/pydatastructs/graphs/adjacency_matrix.py index d8135a992..5c25e7fb0 100644 --- a/pydatastructs/graphs/adjacency_matrix.py +++ b/pydatastructs/graphs/adjacency_matrix.py @@ -1,6 +1,6 @@ from pydatastructs.graphs.graph import Graph from pydatastructs.linear_data_structures import OneDimensionalArray -from pydatastructs.utils.misc_util import AdjacencyMatrixGraphNode +from pydatastructs.utils.misc_util import AdjacencyMatrixGraphNode, GraphEdge __all__ = [ 'AdjacencyMatrix' @@ -18,11 +18,9 @@ class AdjacencyMatrix(Graph): def __new__(cls, *vertices): obj = object.__new__(cls) num_vertices = len(vertices) - obj.vertices = OneDimensionalArray( - AdjacencyMatrixGraphNode, - num_vertices) + obj.vertices = [vertex.name for vertex in vertices] for vertex in vertices: - obj.vertices[vertex.name] = vertex + obj.__setattr__(str(vertex.name), vertex) obj.matrix = OneDimensionalArray( OneDimensionalArray, num_vertices) @@ -31,6 +29,7 @@ def __new__(cls, *vertices): bool, num_vertices) obj.matrix[i].fill(False) + obj.edge_weights = dict() return obj def is_adjacent(self, node1, node2): @@ -40,7 +39,8 @@ def neighbors(self, node): neighbors = [] for i in range(self.matrix[node]._size): if self.matrix[node][i]: - neighbors.append(self.vertices[i]) + neighbors.append(self.__getattribute__( + str(self.vertices[i]))) return neighbors def add_vertex(self, node): @@ -51,8 +51,20 @@ def remove_vertex(self, node): raise NotImplementedError("Currently we allow " "adjacency matrix for static graphs only.") - def add_edge(self, source, target): + def add_edge(self, source, target, cost=None): self.matrix[source][target] = True + source, target = str(source), str(target) + if cost is not None: + self.edge_weights[source + "_" + target] = \ + GraphEdge(self.__getattribute__(source), + self.__getattribute__(target), + cost) + + def get_edge(self, source, target): + return self.edge_weights.get( + str(source) + "_" + str(target), + None) def remove_edge(self, source, target): self.matrix[source][target] = False + self.edge_weights.pop(str(source) + "_" + str(target), None) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index e8c813b90..f38540312 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -3,12 +3,15 @@ data structure. """ from collections import deque as Queue -from pydatastructs.utils.misc_util import AdjacencyListGraphNode from concurrent.futures import ThreadPoolExecutor +from pydatastructs.utils import GraphEdge +from pydatastructs.miscellaneous_data_structures import DisjointSetForest +from pydatastructs.graphs.graph import Graph __all__ = [ 'breadth_first_search', - 'breadth_first_search_parallel' + 'breadth_first_search_parallel', + 'minimum_spanning_tree' ] def breadth_first_search( @@ -186,3 +189,83 @@ def _breadth_first_search_parallel_adjacency_list( return None _breadth_first_search_parallel_adjacency_matrix = _breadth_first_search_parallel_adjacency_list + +def _minimum_spanning_tree_kruskal_adjacency_list(graph): + mst = Graph(*[getattr(graph, v) for v in graph.vertices]) + sort_key = lambda item: item[1].value + dsf = DisjointSetForest() + for v in graph.vertices: + dsf.make_set(v) + for _, edge in sorted(graph.edge_weights.items(), key=sort_key): + u, v = edge.source.name, edge.target.name + if dsf.find_root(u) is not dsf.find_root(v): + mst.add_edge(u, v, edge.value) + dsf.union(u, v) + return mst + +def _minimum_spanning_tree_kruskal_adjacency_matrix(graph): + mst = Graph(*[getattr(graph, str(v)) for v in graph.vertices]) + sort_key = lambda item: item[1].value + dsf = DisjointSetForest() + for v in graph.vertices: + dsf.make_set(v) + for _, edge in sorted(graph.edge_weights.items(), key=sort_key): + u, v = edge.source.name, edge.target.name + if dsf.find_root(u) is not dsf.find_root(v): + mst.add_edge(u, v, edge.value) + dsf.union(u, v) + return mst + +def minimum_spanning_tree(graph, algorithm): + """ + Computes a minimum spanning tree for the given + graph and algorithm. + + Parameters + ========== + + graph: Graph + The graph whose minimum spanning tree + has to be computed. + algorithm: str + The algorithm which should be used for + computing a minimum spanning tree. + Currently the following algorithms are + supported, + 'kruskal' -> Kruskal's algorithm as given in + [1]. + + Returns + ======= + + mst: Graph + A minimum spanning tree using the implementation + same as the graph provided in the input. + + Examples + ======== + + >>> from pydatastructs import Graph, AdjacencyListGraphNode + >>> from pydatastructs import minimum_spanning_tree + >>> u = AdjacencyListGraphNode('u') + >>> v = AdjacencyListGraphNode('v') + >>> G = Graph(u, v) + >>> G.add_edge(u.name, v.name, 3) + >>> mst = minimum_spanning_tree(G, 'kruskal') + >>> u_n = mst.neighbors(u.name) + >>> mst.get_edge(u.name, u_n[0].name).value + 3 + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Kruskal%27s_algorithm + """ + import pydatastructs.graphs.algorithms as algorithms + func = "_minimum_spanning_tree_" + algorithm + "_" + graph._impl + if not hasattr(algorithms, func): + raise NotImplementedError( + "Currently %s algoithm for %s implementation of graphs " + "isn't implemented for finding minimum spanning trees." + %(algorithm, graph._impl)) + return getattr(algorithms, func)(graph) diff --git a/pydatastructs/graphs/graph.py b/pydatastructs/graphs/graph.py index f274aeb10..16bad5675 100644 --- a/pydatastructs/graphs/graph.py +++ b/pydatastructs/graphs/graph.py @@ -96,7 +96,7 @@ def remove_vertex(self, node): raise NotImplementedError( "This is an abstract method.") - def add_edge(self, source, target): + def add_edge(self, source, target, cost=None): """ Adds the edge starting at first parameter i.e., source and ending at the second @@ -105,6 +105,15 @@ def add_edge(self, source, target): raise NotImplementedError( "This is an abstract method.") + def get_edge(self, source, target): + """ + Returns GraphEdge object if there + is an edge between source and target + otherwise None. + """ + raise NotImplementedError( + "This is an abstract method.") + def remove_edge(self, source, target): """ Removes the edge starting at first parameter diff --git a/pydatastructs/graphs/tests/test_adjacency_list.py b/pydatastructs/graphs/tests/test_adjacency_list.py index 5581e1430..d1635fc83 100644 --- a/pydatastructs/graphs/tests/test_adjacency_list.py +++ b/pydatastructs/graphs/tests/test_adjacency_list.py @@ -21,12 +21,18 @@ def test_adjacency_list(): assert neighbors == [v_2] v = AdjacencyListGraphNode('v', 4) g.add_vertex(v) - g.add_edge('v_1', 'v') - g.add_edge('v_2', 'v') - g.add_edge('v_3', 'v') + g.add_edge('v_1', 'v', 0) + g.add_edge('v_2', 'v', 0) + g.add_edge('v_3', 'v', 0) assert g.is_adjacent('v_1', 'v') is True assert g.is_adjacent('v_2', 'v') is True assert g.is_adjacent('v_3', 'v') is True + e1 = g.get_edge('v_1', 'v') + e2 = g.get_edge('v_2', 'v') + e3 = g.get_edge('v_3', 'v') + assert (e1.source.name, e1.target.name) == ('v_1', 'v') + assert (e2.source.name, e2.target.name) == ('v_2', 'v') + assert (e3.source.name, e3.target.name) == ('v_3', 'v') g.remove_edge('v_1', 'v') assert g.is_adjacent('v_1', 'v') is False g.remove_vertex('v') diff --git a/pydatastructs/graphs/tests/test_adjacency_matrix.py b/pydatastructs/graphs/tests/test_adjacency_matrix.py index b6f9c3faf..5fe8c72d5 100644 --- a/pydatastructs/graphs/tests/test_adjacency_matrix.py +++ b/pydatastructs/graphs/tests/test_adjacency_matrix.py @@ -5,10 +5,16 @@ def test_AdjacencyMatrix(): v_0 = AdjacencyMatrixGraphNode(0, 0) v_1 = AdjacencyMatrixGraphNode(1, 1) v_2 = AdjacencyMatrixGraphNode(2, 2) - g = Graph(v_0, v_1, v_2, implementation='adjacency_matrix') - g.add_edge(0, 1) - g.add_edge(1, 2) - g.add_edge(2, 0) + g = Graph(v_0, v_1, v_2) + g.add_edge(0, 1, 0) + g.add_edge(1, 2, 0) + g.add_edge(2, 0, 0) + e1 = g.get_edge(0, 1) + e2 = g.get_edge(1, 2) + e3 = g.get_edge(2, 0) + assert (e1.source.name, e1.target.name) == (0, 1) + assert (e2.source.name, e2.target.name) == (1, 2) + assert (e3.source.name, e3.target.name) == (2, 0) assert g.is_adjacent(0, 1) is True assert g.is_adjacent(1, 2) is True assert g.is_adjacent(2, 0) is True diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index 065fa2e94..a5c28ea85 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -1,10 +1,10 @@ from pydatastructs import (breadth_first_search, Graph, -breadth_first_search_parallel) +breadth_first_search_parallel, minimum_spanning_tree) def test_breadth_first_search(): - def _test_breadth_first_search(ds, impl): + def _test_breadth_first_search(ds): import pydatastructs.utils.misc_util as utils GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") @@ -12,7 +12,7 @@ def _test_breadth_first_search(ds, impl): V2 = GraphNode(1) V3 = GraphNode(2) - G1 = Graph(V1, V2, V3, implementation=impl) + G1 = Graph(V1, V2, V3) edges = [ (V1.name, V2.name), @@ -47,7 +47,7 @@ def bfs_tree(curr_node, next_node, parent): (V7.name, V8.name) ] - G2 = Graph(V4, V5, V6, V7, V8, implementation=impl) + G2 = Graph(V4, V5, V6, V7, V8) for edge in edges: G2.add_edge(*edge) @@ -71,12 +71,12 @@ def path_finder(curr_node, next_node, dest_node, parent, path): breadth_first_search(G2, V4.name, path_finder, V7.name, parent, path) assert path == [V4.name, V5.name, V6.name, V7.name] - _test_breadth_first_search("List", "adjacency_list") - _test_breadth_first_search("Matrix", "adjacency_matrix") + _test_breadth_first_search("List") + _test_breadth_first_search("Matrix") def test_breadth_first_search_parallel(): - def _test_breadth_first_search_parallel(ds, impl): + def _test_breadth_first_search_parallel(ds): import pydatastructs.utils.misc_util as utils GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") @@ -90,7 +90,7 @@ def _test_breadth_first_search_parallel(ds, impl): V8 = GraphNode(7) - G1 = Graph(V1, V2, V3, V4, V5, V6, V7, V8, implementation=impl) + G1 = Graph(V1, V2, V3, V4, V5, V6, V7, V8) edges = [ (V1.name, V2.name), @@ -119,5 +119,32 @@ def bfs_tree(curr_node, next_node, parent): (parent[V6.name] in (V2.name, V3.name)) and (parent[V7.name] in (V3.name, V4.name)) and (parent[V8.name] == V4.name)) - _test_breadth_first_search_parallel("List", "adjacency_list") - _test_breadth_first_search_parallel("Matrix", "adjacency_matrix") + _test_breadth_first_search_parallel("List") + _test_breadth_first_search_parallel("Matrix") + +def test_minimum_spanning_tree(): + + def _test_minimum_spanning_tree(ds, algorithm): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + a, b, c, d, e = [GraphNode(x) for x in [0, 1, 2, 3, 4]] + graph = Graph(a, b, c, d, e) + graph.add_edge(a.name, c.name, 10) + graph.add_edge(c.name, a.name, 10) + graph.add_edge(a.name, d.name, 7) + graph.add_edge(d.name, a.name, 7) + graph.add_edge(c.name, d.name, 9) + graph.add_edge(d.name, c.name, 9) + graph.add_edge(d.name, b.name, 32) + graph.add_edge(b.name, d.name, 32) + graph.add_edge(d.name, e.name, 23) + graph.add_edge(e.name, d.name, 23) + mst = minimum_spanning_tree(graph, algorithm=algorithm) + expected_mst = [('0_3', 7), ('2_3', 9), ('3_4', 23), ('3_1', 32), + ('3_0', 7), ('3_2', 9), ('4_3', 23), ('1_3', 32)] + assert len(expected_mst) == 2*len(mst.edge_weights.items()) + for k, v in mst.edge_weights.items(): + assert (k, v.value) in expected_mst + + _test_minimum_spanning_tree("List", "kruskal") + _test_minimum_spanning_tree("Matrix", "kruskal")