0

Here is this graph algorithm that finds a path between two nodes in a DAG.

from typing import *
def find_path(graph: List[List[int]], start: int, end: int) -> List[int]:
    path = []
    def dfs(node):
        path.append(node)
        if node == end: return True
        for neighbor in graph[node]:
            if dfs(neighbor): return True
            path.pop()
        return False

    dfs(start)
    return path

I was wondering if this code could be turned into an iterative DFS.

Here is a sample input:

graph = [[1,2],[3],[3],[]]
start = 0
end = 3
find_path(graph, start, end)
  • The first algorithm is no correct. I suppose you made a mistake with the indentation. – trincot Sep 24 '22 at 15:00
  • @trincot A DAG (directed *acyclic* graph) has no cycles by definition. – chepner Sep 24 '22 at 15:41
  • In general, you use a while loop over which you repeated pop a node from a stack, then push all its neighbors onto the stack. – chepner Sep 24 '22 at 15:48
  • I hadn't noticed the mention of "DAG", @chepner, but looking at the history, that info was added after I made the comment. – trincot Sep 24 '22 at 16:04

2 Answers2

0

Iteration requires a stack:

#  from https://stackoverflow.com/a/39376201/1911064
def dfs_paths(graph, start, goal):
    stack = [(start, [start])]
    visited = set()
    while stack:
        (vertex, path) = stack.pop()
        if vertex not in visited:
            if vertex == goal:
                return path
            visited.add(vertex)
            for neighbor in graph[vertex]:
                stack.append((neighbor, path + [neighbor]))
 
graph = [[1,2],[3],[3],[]]
start = 0
end = 3
path = dfs_paths(graph, start, end)

for i in path:
    print(i)

Here, stack entries are tuples combining a vertex and a partial path. Whenever a new node is pushed on the stack, the partial path of the previous node is extended by the new node.


Alternative with reduced memory consumption:

def dfs_paths(graph, start, goal):
    stack = [start]
    predecessor = [-1 for i in len(graph)]
    visited = set()
    while stack:
        vertex = stack.pop()        
        if vertex not in visited:
            if vertex == goal:
                return path
            visited.add(vertex)
            for neighbor in graph[vertex]:
                predecessor[neighbor] = vertex
                stack.append(neighbor)
    return predecessor
Axel Kemper
  • 10,544
  • 2
  • 31
  • 54
  • Thank you for this solution. I knew about this way of doing it, sadly it's not equivalent. The memory consumption is way higher since for the recursive DFS you just need one list to track the path. For this solution each node tracks the path it traverses; making it unsuitable for large graphs. –  Sep 24 '22 at 15:59
  • @IsmailMaj can I ask your motivation for making iterative rather than recursive? – Mulan Sep 24 '22 at 17:32
  • @Mulan performance and memory consumption –  Sep 24 '22 at 17:50
0

You could stack the node together with an iterator object that iterates its neighbors. Since your original code does not use visited flags (to avoid revisiting the same node), I will not use them here either:

def find_path(graph: List[List[int]], start: int, end: int) -> List[int]:
    stack = [[start, iter(graph[start])]]
    while stack:
        neighbor = next(stack[-1][1], None)
        if neighbor:
            stack.append([neighbor, iter(graph[neighbor])])
            if neighbor == end:
                return [node for node, _ in stack]
        else:
            stack.pop()
trincot
  • 317,000
  • 35
  • 244
  • 286