Skip to content

Commit 4845b22

Browse files
authored
Added sequential Kurskal Algorithm (#168)
* Along with adding Kruskal algorithm Graph API has been modified to allow storing edge weights. * `Graph.get_edge` to obtain information about edges in the graph.
1 parent 835aeef commit 4845b22

9 files changed

+186
-30
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ sudo: false
33
language: python
44

55
python:
6-
- "3.5"
6+
- "3.6"
77
install:
88
- pip install -r requirements.txt
99

pydatastructs/graphs/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from . import algorithms
1010
from .algorithms import (
1111
breadth_first_search,
12-
breadth_first_search_parallel
12+
breadth_first_search_parallel,
13+
minimum_spanning_tree
1314
)
1415

1516
__all__.extend(algorithms.__all__)

pydatastructs/graphs/adjacency_list.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pydatastructs.graphs.graph import Graph
22
from pydatastructs.linear_data_structures import DynamicOneDimensionalArray
3+
from pydatastructs.utils.misc_util import GraphEdge
34

45
__all__ = [
56
'AdjacencyList'
@@ -19,6 +20,7 @@ def __new__(cls, *vertices):
1920
for vertex in vertices:
2021
obj.__setattr__(vertex.name, vertex)
2122
obj.vertices = set([vertex.name for vertex in vertices])
23+
obj.edge_weights = dict()
2224
return obj
2325

2426
def is_adjacent(self, node1, node2):
@@ -42,14 +44,24 @@ def remove_vertex(self, name):
4244
delattr(node_obj, name)
4345
node_obj.adjacent.remove(name)
4446

45-
def add_edge(self, source, target):
47+
def add_edge(self, source, target, cost=None):
4648
source, target = self.__getattribute__(source), \
4749
self.__getattribute__(target)
4850
source.__setattr__(target.name, target)
4951
source.adjacent.add(target.name)
52+
if cost is not None:
53+
self.edge_weights[source.name + "_" + target.name] = \
54+
GraphEdge(source, target, cost)
55+
56+
def get_edge(self, source, target):
57+
return self.edge_weights.get(
58+
source + "_" + target,
59+
None)
5060

5161
def remove_edge(self, source, target):
5262
source, target = self.__getattribute__(source), \
5363
self.__getattribute__(target)
5464
source.adjacent.remove(target.name)
5565
delattr(source, target.name)
66+
self.edge_weights.pop(source.name + "_" + target.name,
67+
None)

pydatastructs/graphs/adjacency_matrix.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pydatastructs.graphs.graph import Graph
22
from pydatastructs.linear_data_structures import OneDimensionalArray
3-
from pydatastructs.utils.misc_util import AdjacencyMatrixGraphNode
3+
from pydatastructs.utils.misc_util import AdjacencyMatrixGraphNode, GraphEdge
44

55
__all__ = [
66
'AdjacencyMatrix'
@@ -18,11 +18,9 @@ class AdjacencyMatrix(Graph):
1818
def __new__(cls, *vertices):
1919
obj = object.__new__(cls)
2020
num_vertices = len(vertices)
21-
obj.vertices = OneDimensionalArray(
22-
AdjacencyMatrixGraphNode,
23-
num_vertices)
21+
obj.vertices = [vertex.name for vertex in vertices]
2422
for vertex in vertices:
25-
obj.vertices[vertex.name] = vertex
23+
obj.__setattr__(str(vertex.name), vertex)
2624
obj.matrix = OneDimensionalArray(
2725
OneDimensionalArray,
2826
num_vertices)
@@ -31,6 +29,7 @@ def __new__(cls, *vertices):
3129
bool,
3230
num_vertices)
3331
obj.matrix[i].fill(False)
32+
obj.edge_weights = dict()
3433
return obj
3534

3635
def is_adjacent(self, node1, node2):
@@ -40,7 +39,8 @@ def neighbors(self, node):
4039
neighbors = []
4140
for i in range(self.matrix[node]._size):
4241
if self.matrix[node][i]:
43-
neighbors.append(self.vertices[i])
42+
neighbors.append(self.__getattribute__(
43+
str(self.vertices[i])))
4444
return neighbors
4545

4646
def add_vertex(self, node):
@@ -51,8 +51,20 @@ def remove_vertex(self, node):
5151
raise NotImplementedError("Currently we allow "
5252
"adjacency matrix for static graphs only.")
5353

54-
def add_edge(self, source, target):
54+
def add_edge(self, source, target, cost=None):
5555
self.matrix[source][target] = True
56+
source, target = str(source), str(target)
57+
if cost is not None:
58+
self.edge_weights[source + "_" + target] = \
59+
GraphEdge(self.__getattribute__(source),
60+
self.__getattribute__(target),
61+
cost)
62+
63+
def get_edge(self, source, target):
64+
return self.edge_weights.get(
65+
str(source) + "_" + str(target),
66+
None)
5667

5768
def remove_edge(self, source, target):
5869
self.matrix[source][target] = False
70+
self.edge_weights.pop(str(source) + "_" + str(target), None)

pydatastructs/graphs/algorithms.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
data structure.
44
"""
55
from collections import deque as Queue
6-
from pydatastructs.utils.misc_util import AdjacencyListGraphNode
76
from concurrent.futures import ThreadPoolExecutor
7+
from pydatastructs.utils import GraphEdge
8+
from pydatastructs.miscellaneous_data_structures import DisjointSetForest
9+
from pydatastructs.graphs.graph import Graph
810

911
__all__ = [
1012
'breadth_first_search',
11-
'breadth_first_search_parallel'
13+
'breadth_first_search_parallel',
14+
'minimum_spanning_tree'
1215
]
1316

1417
def breadth_first_search(
@@ -186,3 +189,83 @@ def _breadth_first_search_parallel_adjacency_list(
186189
return None
187190

188191
_breadth_first_search_parallel_adjacency_matrix = _breadth_first_search_parallel_adjacency_list
192+
193+
def _minimum_spanning_tree_kruskal_adjacency_list(graph):
194+
mst = Graph(*[getattr(graph, v) for v in graph.vertices])
195+
sort_key = lambda item: item[1].value
196+
dsf = DisjointSetForest()
197+
for v in graph.vertices:
198+
dsf.make_set(v)
199+
for _, edge in sorted(graph.edge_weights.items(), key=sort_key):
200+
u, v = edge.source.name, edge.target.name
201+
if dsf.find_root(u) is not dsf.find_root(v):
202+
mst.add_edge(u, v, edge.value)
203+
dsf.union(u, v)
204+
return mst
205+
206+
def _minimum_spanning_tree_kruskal_adjacency_matrix(graph):
207+
mst = Graph(*[getattr(graph, str(v)) for v in graph.vertices])
208+
sort_key = lambda item: item[1].value
209+
dsf = DisjointSetForest()
210+
for v in graph.vertices:
211+
dsf.make_set(v)
212+
for _, edge in sorted(graph.edge_weights.items(), key=sort_key):
213+
u, v = edge.source.name, edge.target.name
214+
if dsf.find_root(u) is not dsf.find_root(v):
215+
mst.add_edge(u, v, edge.value)
216+
dsf.union(u, v)
217+
return mst
218+
219+
def minimum_spanning_tree(graph, algorithm):
220+
"""
221+
Computes a minimum spanning tree for the given
222+
graph and algorithm.
223+
224+
Parameters
225+
==========
226+
227+
graph: Graph
228+
The graph whose minimum spanning tree
229+
has to be computed.
230+
algorithm: str
231+
The algorithm which should be used for
232+
computing a minimum spanning tree.
233+
Currently the following algorithms are
234+
supported,
235+
'kruskal' -> Kruskal's algorithm as given in
236+
[1].
237+
238+
Returns
239+
=======
240+
241+
mst: Graph
242+
A minimum spanning tree using the implementation
243+
same as the graph provided in the input.
244+
245+
Examples
246+
========
247+
248+
>>> from pydatastructs import Graph, AdjacencyListGraphNode
249+
>>> from pydatastructs import minimum_spanning_tree
250+
>>> u = AdjacencyListGraphNode('u')
251+
>>> v = AdjacencyListGraphNode('v')
252+
>>> G = Graph(u, v)
253+
>>> G.add_edge(u.name, v.name, 3)
254+
>>> mst = minimum_spanning_tree(G, 'kruskal')
255+
>>> u_n = mst.neighbors(u.name)
256+
>>> mst.get_edge(u.name, u_n[0].name).value
257+
3
258+
259+
References
260+
==========
261+
262+
.. [1] https://en.wikipedia.org/wiki/Kruskal%27s_algorithm
263+
"""
264+
import pydatastructs.graphs.algorithms as algorithms
265+
func = "_minimum_spanning_tree_" + algorithm + "_" + graph._impl
266+
if not hasattr(algorithms, func):
267+
raise NotImplementedError(
268+
"Currently %s algoithm for %s implementation of graphs "
269+
"isn't implemented for finding minimum spanning trees."
270+
%(algorithm, graph._impl))
271+
return getattr(algorithms, func)(graph)

pydatastructs/graphs/graph.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def remove_vertex(self, node):
9696
raise NotImplementedError(
9797
"This is an abstract method.")
9898

99-
def add_edge(self, source, target):
99+
def add_edge(self, source, target, cost=None):
100100
"""
101101
Adds the edge starting at first parameter
102102
i.e., source and ending at the second
@@ -105,6 +105,15 @@ def add_edge(self, source, target):
105105
raise NotImplementedError(
106106
"This is an abstract method.")
107107

108+
def get_edge(self, source, target):
109+
"""
110+
Returns GraphEdge object if there
111+
is an edge between source and target
112+
otherwise None.
113+
"""
114+
raise NotImplementedError(
115+
"This is an abstract method.")
116+
108117
def remove_edge(self, source, target):
109118
"""
110119
Removes the edge starting at first parameter

pydatastructs/graphs/tests/test_adjacency_list.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,18 @@ def test_adjacency_list():
2121
assert neighbors == [v_2]
2222
v = AdjacencyListGraphNode('v', 4)
2323
g.add_vertex(v)
24-
g.add_edge('v_1', 'v')
25-
g.add_edge('v_2', 'v')
26-
g.add_edge('v_3', 'v')
24+
g.add_edge('v_1', 'v', 0)
25+
g.add_edge('v_2', 'v', 0)
26+
g.add_edge('v_3', 'v', 0)
2727
assert g.is_adjacent('v_1', 'v') is True
2828
assert g.is_adjacent('v_2', 'v') is True
2929
assert g.is_adjacent('v_3', 'v') is True
30+
e1 = g.get_edge('v_1', 'v')
31+
e2 = g.get_edge('v_2', 'v')
32+
e3 = g.get_edge('v_3', 'v')
33+
assert (e1.source.name, e1.target.name) == ('v_1', 'v')
34+
assert (e2.source.name, e2.target.name) == ('v_2', 'v')
35+
assert (e3.source.name, e3.target.name) == ('v_3', 'v')
3036
g.remove_edge('v_1', 'v')
3137
assert g.is_adjacent('v_1', 'v') is False
3238
g.remove_vertex('v')

pydatastructs/graphs/tests/test_adjacency_matrix.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ def test_AdjacencyMatrix():
55
v_0 = AdjacencyMatrixGraphNode(0, 0)
66
v_1 = AdjacencyMatrixGraphNode(1, 1)
77
v_2 = AdjacencyMatrixGraphNode(2, 2)
8-
g = Graph(v_0, v_1, v_2, implementation='adjacency_matrix')
9-
g.add_edge(0, 1)
10-
g.add_edge(1, 2)
11-
g.add_edge(2, 0)
8+
g = Graph(v_0, v_1, v_2)
9+
g.add_edge(0, 1, 0)
10+
g.add_edge(1, 2, 0)
11+
g.add_edge(2, 0, 0)
12+
e1 = g.get_edge(0, 1)
13+
e2 = g.get_edge(1, 2)
14+
e3 = g.get_edge(2, 0)
15+
assert (e1.source.name, e1.target.name) == (0, 1)
16+
assert (e2.source.name, e2.target.name) == (1, 2)
17+
assert (e3.source.name, e3.target.name) == (2, 0)
1218
assert g.is_adjacent(0, 1) is True
1319
assert g.is_adjacent(1, 2) is True
1420
assert g.is_adjacent(2, 0) is True

pydatastructs/graphs/tests/test_algorithms.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
from pydatastructs import (breadth_first_search, Graph,
2-
breadth_first_search_parallel)
2+
breadth_first_search_parallel, minimum_spanning_tree)
33

44

55
def test_breadth_first_search():
66

7-
def _test_breadth_first_search(ds, impl):
7+
def _test_breadth_first_search(ds):
88
import pydatastructs.utils.misc_util as utils
99
GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode")
1010

1111
V1 = GraphNode(0)
1212
V2 = GraphNode(1)
1313
V3 = GraphNode(2)
1414

15-
G1 = Graph(V1, V2, V3, implementation=impl)
15+
G1 = Graph(V1, V2, V3)
1616

1717
edges = [
1818
(V1.name, V2.name),
@@ -47,7 +47,7 @@ def bfs_tree(curr_node, next_node, parent):
4747
(V7.name, V8.name)
4848
]
4949

50-
G2 = Graph(V4, V5, V6, V7, V8, implementation=impl)
50+
G2 = Graph(V4, V5, V6, V7, V8)
5151

5252
for edge in edges:
5353
G2.add_edge(*edge)
@@ -71,12 +71,12 @@ def path_finder(curr_node, next_node, dest_node, parent, path):
7171
breadth_first_search(G2, V4.name, path_finder, V7.name, parent, path)
7272
assert path == [V4.name, V5.name, V6.name, V7.name]
7373

74-
_test_breadth_first_search("List", "adjacency_list")
75-
_test_breadth_first_search("Matrix", "adjacency_matrix")
74+
_test_breadth_first_search("List")
75+
_test_breadth_first_search("Matrix")
7676

7777
def test_breadth_first_search_parallel():
7878

79-
def _test_breadth_first_search_parallel(ds, impl):
79+
def _test_breadth_first_search_parallel(ds):
8080
import pydatastructs.utils.misc_util as utils
8181
GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode")
8282

@@ -90,7 +90,7 @@ def _test_breadth_first_search_parallel(ds, impl):
9090
V8 = GraphNode(7)
9191

9292

93-
G1 = Graph(V1, V2, V3, V4, V5, V6, V7, V8, implementation=impl)
93+
G1 = Graph(V1, V2, V3, V4, V5, V6, V7, V8)
9494

9595
edges = [
9696
(V1.name, V2.name),
@@ -119,5 +119,32 @@ def bfs_tree(curr_node, next_node, parent):
119119
(parent[V6.name] in (V2.name, V3.name)) and
120120
(parent[V7.name] in (V3.name, V4.name)) and (parent[V8.name] == V4.name))
121121

122-
_test_breadth_first_search_parallel("List", "adjacency_list")
123-
_test_breadth_first_search_parallel("Matrix", "adjacency_matrix")
122+
_test_breadth_first_search_parallel("List")
123+
_test_breadth_first_search_parallel("Matrix")
124+
125+
def test_minimum_spanning_tree():
126+
127+
def _test_minimum_spanning_tree(ds, algorithm):
128+
import pydatastructs.utils.misc_util as utils
129+
GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode")
130+
a, b, c, d, e = [GraphNode(x) for x in [0, 1, 2, 3, 4]]
131+
graph = Graph(a, b, c, d, e)
132+
graph.add_edge(a.name, c.name, 10)
133+
graph.add_edge(c.name, a.name, 10)
134+
graph.add_edge(a.name, d.name, 7)
135+
graph.add_edge(d.name, a.name, 7)
136+
graph.add_edge(c.name, d.name, 9)
137+
graph.add_edge(d.name, c.name, 9)
138+
graph.add_edge(d.name, b.name, 32)
139+
graph.add_edge(b.name, d.name, 32)
140+
graph.add_edge(d.name, e.name, 23)
141+
graph.add_edge(e.name, d.name, 23)
142+
mst = minimum_spanning_tree(graph, algorithm=algorithm)
143+
expected_mst = [('0_3', 7), ('2_3', 9), ('3_4', 23), ('3_1', 32),
144+
('3_0', 7), ('3_2', 9), ('4_3', 23), ('1_3', 32)]
145+
assert len(expected_mst) == 2*len(mst.edge_weights.items())
146+
for k, v in mst.edge_weights.items():
147+
assert (k, v.value) in expected_mst
148+
149+
_test_minimum_spanning_tree("List", "kruskal")
150+
_test_minimum_spanning_tree("Matrix", "kruskal")

0 commit comments

Comments
 (0)