Skip to content

Commit f936500

Browse files
authored
Merge pull request #61 from mdickinson/feature/shortest-path
Add 'shortest_path' method
2 parents ec93224 + 9257e0d commit f936500

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

refcycle/i_directed_graph.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,46 @@ def ancestors(self, start, generations=None):
196196
)
197197
return self.full_subgraph(visited)
198198

199+
def shortest_path(self, start, end):
200+
"""
201+
Find a shortest path from start to end.
202+
203+
Returns the subgraph consisting of the vertices in that path
204+
and the edges between them.
205+
206+
Raises ValueError if no path from start to end exists.
207+
"""
208+
# Vertices whose children are yet to be explored.
209+
to_visit = deque([start])
210+
211+
# Mapping from each child to the parent that it was first found via.
212+
# We map the start vertex to a dummy object.
213+
dummy = object()
214+
explored = self.vertex_dict()
215+
explored[start] = dummy
216+
217+
# Breadth-first search, rooted at ``start``.
218+
while to_visit:
219+
if end in explored:
220+
break
221+
222+
parent = to_visit.popleft()
223+
for child in self.children(parent):
224+
if child not in explored:
225+
explored[child] = parent
226+
to_visit.append(child)
227+
else:
228+
raise ValueError("No path found.")
229+
230+
# Backtrack to construct vertices of path.
231+
vertex = end
232+
path = []
233+
while vertex is not dummy:
234+
path.append(vertex)
235+
vertex = explored[vertex]
236+
237+
return self.full_subgraph(path)
238+
199239
def _component_graph(self):
200240
"""
201241
Compute the graph of strongly connected components.

refcycle/test/test_object_graph.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,52 @@ def test_ancestors(self):
228228
self.assertCountEqual(graph.ancestors(c), [c, a])
229229
self.assertCountEqual(graph.ancestors(d), [d, b, c, a])
230230

231+
def test_shortest_path(self):
232+
# Looking for paths from a to f, we have:
233+
# a -> b -> e -> f
234+
# a -> c -> f (shortest)
235+
a = []
236+
b = []
237+
c = []
238+
d = []
239+
e = []
240+
f = []
241+
a.append(b)
242+
a.append(c)
243+
a.append(d)
244+
b.append(e)
245+
e.append(f)
246+
c.append(f)
247+
graph = ObjectGraph([a, b, c, d, e, f])
248+
path = graph.shortest_path(a, f)
249+
self.assertIsInstance(path, IDirectedGraph)
250+
self.assertEqual(len(path.vertices), 3)
251+
252+
self.assertIn(a, path.vertices)
253+
self.assertIn(c, path.vertices)
254+
self.assertIn(f, path.vertices)
255+
256+
def test_shortest_path_no_path(self):
257+
a = []
258+
b = []
259+
c = []
260+
a.append(b)
261+
c.append(b)
262+
graph = ObjectGraph([a, b, c])
263+
with self.assertRaises(ValueError):
264+
graph.shortest_path(a, c)
265+
266+
def test_shortest_path_start_equals_end(self):
267+
a = []
268+
b = []
269+
a.append(b)
270+
b.append(a)
271+
a.append(a)
272+
graph = ObjectGraph([a])
273+
path = graph.shortest_path(a, a)
274+
self.assertIsInstance(path, IDirectedGraph)
275+
self.assertEqual(len(path.vertices), 1)
276+
231277
def test_to_dot(self):
232278
a = []
233279
b = []

0 commit comments

Comments
 (0)