Skip to content

Commit 921d553

Browse files
committed
Fix non-linear running time in descendants and ancestors methods.
1 parent f936500 commit 921d553

2 files changed

Lines changed: 48 additions & 12 deletions

File tree

refcycle/i_directed_graph.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,16 @@ def descendants(self, start, generations=None):
164164
165165
"""
166166
visited = self.vertex_set()
167+
visited.add(start)
167168
to_visit = deque([(start, 0)])
168169
while to_visit:
169170
vertex, depth = to_visit.popleft()
170-
visited.add(vertex)
171-
if generations is None or depth < generations:
172-
to_visit.extend(
173-
(child, depth+1) for child in self.children(vertex)
174-
if child not in visited
175-
)
171+
if depth == generations:
172+
continue
173+
for child in self.children(vertex):
174+
if child not in visited:
175+
visited.add(child)
176+
to_visit.append((child, depth+1))
176177
return self.full_subgraph(visited)
177178

178179
def ancestors(self, start, generations=None):
@@ -185,15 +186,16 @@ def ancestors(self, start, generations=None):
185186
186187
"""
187188
visited = self.vertex_set()
189+
visited.add(start)
188190
to_visit = deque([(start, 0)])
189191
while to_visit:
190192
vertex, depth = to_visit.popleft()
191-
visited.add(vertex)
192-
if generations is None or depth < generations:
193-
to_visit.extend(
194-
(parent, depth+1) for parent in self.parents(vertex)
195-
if parent not in visited
196-
)
193+
if depth == generations:
194+
continue
195+
for parent in self.parents(vertex):
196+
if parent not in visited:
197+
visited.add(parent)
198+
to_visit.append((parent, depth+1))
197199
return self.full_subgraph(visited)
198200

199201
def shortest_path(self, start, end):

refcycle/test/test_directed_graph.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,23 @@ def test_more_limited_descendants(self):
199199
list('12345678'),
200200
)
201201

202+
def test_descendants_slow_case(self):
203+
# Regression test for #48. A buggy earlier version of the
204+
# descendants method had running time exponential in
205+
# vertex_count.
206+
vertex_count = 100
207+
vertices = set(range(vertex_count))
208+
edge_mapper = {
209+
n: [(n + 1) % vertex_count, (n + 1) % vertex_count]
210+
for n in vertices
211+
}
212+
graph = DirectedGraph.from_out_edges(
213+
vertices=vertices,
214+
edge_mapper=edge_mapper,
215+
)
216+
descendants = graph.descendants(0)
217+
self.assertEqual(set(descendants), vertices)
218+
202219
def test_limited_ancestors(self):
203220
graph = graph_from_string(
204221
"1 2 3 4 5 6; 1->2 1->3 2->3 2->4 4->3 4->5 5->2 5->6 6->3 6->4")
@@ -220,6 +237,23 @@ def test_limited_ancestors(self):
220237
['1', '2', '3', '4', '5', '6'],
221238
)
222239

240+
def test_ancestors_slow_case(self):
241+
# Regression test for #48. A buggy earlier version of the
242+
# ancestors method had running time exponential in
243+
# vertex_count.
244+
vertex_count = 100
245+
vertices = set(range(vertex_count))
246+
edge_mapper = {
247+
n: [(n + 1) % vertex_count, (n + 1) % vertex_count]
248+
for n in vertices
249+
}
250+
graph = DirectedGraph.from_out_edges(
251+
vertices=vertices,
252+
edge_mapper=edge_mapper,
253+
)
254+
ancestors = graph.ancestors(0)
255+
self.assertEqual(set(ancestors), vertices)
256+
223257
def test_length(self):
224258
self.assertEqual(len(test_graph), 11)
225259

0 commit comments

Comments
 (0)