import unittest from collections import deque from heapq import heappush, heappop ################################################################################ # InterviewCake.com ################################################################################ # construct_path :: Map String String -> String -> String -> [String] def construct_path(paths, beg, end): """ Reconstruct the path from `beg` to `end`. """ result = [] current = end print(paths) print(beg, end) print('-----') while current: result.append(current) current = paths[current] result.reverse() return result def get_path_ic(graph, beg, end): """ InterviewCake uses a dictionary and back-tracking to store and reconstruct the path instead of storing the path as state on each node. This reduces the memory costs. See get_path_bft for an example of this less optimal solution. """ if beg not in graph: raise Exception('Origin node absent from graph.') if end not in graph: raise Exception('Destination node absent from graph.') q = deque() q.append(beg) paths = {beg: None} while q: node = q.popleft() if node == end: print(graph) return construct_path(paths, beg, end) for x in graph[node]: if x not in paths: paths[x] = node q.append(x) return None ################################################################################ # Per-node state ################################################################################ def get_path_bft(graph, beg, end): """ Here we find the shortest path from `beg` to `end` in `graph` by doing a BFT from beg to end and storing the path state alongside each node in the queue. """ if beg not in graph: raise Exception('Origin node absent from graph.') if end not in graph: raise Exception('Destination node absent from graph.') q = deque() seen = set() q.append([beg]) while q: path = q.popleft() node = path[-1] seen.add(node) if node == end: return path for x in graph[node]: if x not in seen: q.append(path + [x]) ################################################################################ # Dijkstra's Algorithm ################################################################################ def get_path(graph, beg, end): """ Here we find the shortest path using Dijkstra's algorithm, which is my favorite solution. """ if beg not in graph: raise Exception( 'The origin node, {}, is not present in the graph'.format(beg)) if end not in graph: raise Exception( 'The origin node, {}, is not present in the graph'.format(end)) q = [] seen = set() heappush(q, (1, [beg])) while q: weight, path = heappop(q) node = path[-1] seen.add(node) if node == end: return path for x in graph[node]: if x not in seen: heappush(q, (weight + 1, path + [x])) return None # Tests class Test(unittest.TestCase): def setUp(self): self.graph = { 'a': ['b', 'c', 'd'], 'b': ['a', 'd'], 'c': ['a', 'e'], 'd': ['b', 'a'], 'e': ['c'], 'f': ['g'], 'g': ['f'], } def test_two_hop_path_1(self): actual = get_path(self.graph, 'a', 'e') expected = ['a', 'c', 'e'] self.assertEqual(actual, expected) def test_two_hop_path_2(self): actual = get_path(self.graph, 'd', 'c') expected = ['d', 'a', 'c'] self.assertEqual(actual, expected) def test_one_hop_path_1(self): actual = get_path(self.graph, 'a', 'c') expected = ['a', 'c'] self.assertEqual(actual, expected) def test_one_hop_path_2(self): actual = get_path(self.graph, 'f', 'g') expected = ['f', 'g'] self.assertEqual(actual, expected) def test_one_hop_path_3(self): actual = get_path(self.graph, 'g', 'f') expected = ['g', 'f'] self.assertEqual(actual, expected) def test_zero_hop_path(self): actual = get_path(self.graph, 'a', 'a') expected = ['a'] self.assertEqual(actual, expected) def test_no_path(self): actual = get_path(self.graph, 'a', 'f') expected = None self.assertEqual(actual, expected) def test_start_node_not_present(self): with self.assertRaises(Exception): get_path(self.graph, 'h', 'a') def test_end_node_not_present(self): with self.assertRaises(Exception): get_path(self.graph, 'a', 'h') unittest.main(verbosity=2)