Skip to content

Commit 163a174

Browse files
committed
Added connected components for digraphs
1 parent c1e396f commit 163a174

File tree

4 files changed

+79
-1
lines changed

4 files changed

+79
-1
lines changed

digraph.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from graph import graph
2+
from copy import deepcopy
23
class digraph(graph):
34
"""
45
Directed Graph class - made of nodes and edges
@@ -7,7 +8,7 @@ class digraph(graph):
78
has_edge, nodes, edges, add_node_attribute, node_attributes
89
neighbors, del_node, del_edge, node_order, set_edge_weight,
910
get_edge_weight, set_edge_properties, get_edge_properties
10-
clear_node_attributes, has_cycle
11+
clear_node_attributes, get_transpose
1112
"""
1213

1314
DEFAULT_WEIGHT = 1
@@ -61,3 +62,12 @@ def del_node(self, node):
6162
if self.has_edge((n, node)):
6263
self.del_edge((n, node))
6364
del(self.node_neighbors[node])
65+
66+
def get_transpose(self):
67+
""" Returns the transpose of the graph
68+
with edges reversed and nodes same """
69+
digr = deepcopy(self)
70+
for (u, v) in self.edges():
71+
digr.del_edge((u, v))
72+
digr.add_edge((v, u))
73+
return digr

digraph_test.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,13 @@ def test_clear_node_attributes_method(self):
9494
self.gr.clear_node_attributes()
9595
self.assertEqual(self.gr.node_attributes("a"), [])
9696

97+
def test_get_transpose_method(self):
98+
transpose = self.gr.get_transpose()
99+
self.assertEqual(len(transpose.nodes()), len(self.gr.nodes()))
100+
self.assertEqual(len(transpose.edges()), len(self.gr.edges()))
101+
self.assertTrue(self.gr.has_edge(("a", "b")))
102+
self.assertTrue(transpose.has_edge(("b", "a")))
103+
self.assertFalse(transpose.has_edge(("a", "b")))
104+
97105
if __name__ == "__main__":
98106
unittest.main()

graph_algorithms.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,48 @@ def find_sink_node(digr):
9393
while digr.neighbors(node):
9494
node = digr.neighbors(node)[0]
9595
return node
96+
97+
def directed_connected_components(digr):
98+
""" Returns a list of strongly connected components
99+
in a directed graph using Kosaraju's two pass algorithm """
100+
if not digr.DIRECTED:
101+
raise Exception("%s is not a directed graph" % digr)
102+
finishing_times = DFS_loop(digr.get_transpose())
103+
# use finishing_times in descending order
104+
nodes_explored, connected_components = [], []
105+
for node in finishing_times[::-1]:
106+
component = []
107+
outer_dfs(digr, node, nodes_explored, component)
108+
if component:
109+
nodes_explored += component
110+
connected_components.append(component)
111+
return connected_components
112+
113+
def outer_dfs(digr, node, nodes_explored, path):
114+
if node in path or node in nodes_explored:
115+
return False
116+
path.append(node)
117+
for each in digr.neighbors(node):
118+
if each not in path or each not in nodes_explored:
119+
outer_dfs(digr, each, nodes_explored, path)
120+
121+
def DFS_loop(digr):
122+
""" Core DFS loop used to find strongly connected components
123+
in a directed graph """
124+
node_explored = [] # list for keeping track of nodes explored
125+
finishing_times = [] # list for adding nodes based on their finishing times
126+
for node in digr.nodes():
127+
if node not in node_explored:
128+
leader_node = node
129+
inner_DFS(digr, node, node_explored, finishing_times)
130+
return finishing_times
131+
132+
def inner_DFS(digr, node, node_explored, finishing_times):
133+
""" Inner DFS used in DFS loop method """
134+
node_explored.append(node) # mark explored
135+
for each in digr.neighbors(node):
136+
if each not in node_explored:
137+
inner_DFS(digr, each, node_explored, finishing_times)
138+
global finishing_counter
139+
# adds nodes based on increasing order of finishing times
140+
finishing_times.append(node)

graph_algorithms_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,20 @@ def test_topological_ordering(self):
6464
self.assertEqual(sum([order[u] < order[v] for (u, v) in
6565
dag.edges()]), len(dag.edges())) # all comparisons are True
6666

67+
def test_directed_connected_components(self):
68+
digr = digraph()
69+
digr.add_nodes(["a", "b", "c", "d", "e", "f", "g", "h", "i"])
70+
digr.add_edges([("b", "a"), ("a", "c"), ("c", "b"), ("d", "b")])
71+
digr.add_edges([("d", "f"), ("f", "e"), ("e", "d"), ("g", "e")])
72+
digr.add_edges([("g", "h"), ("h", "i"), ("i", "g")])
73+
self.assertEqual(len(directed_connected_components(digr)), 3)
74+
digr2 = digraph()
75+
digr2.add_nodes(["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"])
76+
digr2.add_edges([("a", "b"), ("b", "c"), ("c", "a"), ("b", "d"), ("d", "e")])
77+
digr2.add_edges([("e", "f"), ("f", "g"), ("g", "e"), ("d", "g"), ("i", "f")])
78+
digr2.add_edges([("h", "g"), ("c", "h"), ("c", "k"), ("h", "i"), ("i", "j")])
79+
digr2.add_edges([("h", "j"), ("j", "k"), ("k", "h")])
80+
self.assertEqual(len(directed_connected_components(digr2)), 4)
81+
6782
if __name__ == "__main__":
6883
unittest.main()

0 commit comments

Comments
 (0)