Search code examples
pythonalgorithmdepth-first-search

Depth-first search with discovery and finish time using Stack but without recursion


I am implementing Depth-first search algorithm. There are two requirements: I must use Stack (no Recursion), and it should also returns the discovery time and finish time. Here is the code using recursion I implemented:

def dfs(graph, source):
    """Depth-first search algorithm

    This function computes depth-first search and prints the nodes as it travels

    Arguments:
        graph: List[List[int]], adjacency matrix of the graph
        source: int, the starting point
    Returns:
        None
    """
    visited = [False] * len(graph)
    time_start = [0] * len(graph) # Time when vertex is discovered
    time_finish = [0] * len(graph) # Time when vertex has finished discovering
    time = 0
    dfs_visit(graph, source, visited, time, time_start, time_finish)
    return time_start, time_finish

def dfs_visit(graph, source, visited, time, time_start, time_finish):
    time += 1
    time_start[source] = time
    visited[source] = True
    print(source, end = " ")
    for i, val in enumerate(graph[source]):
        if not visited[i] and val != 0:
            dfs_visit(graph, i, visited, time, time_start, time_finish)
    time += 1
    time_finish[source] = time

Sample input:

graph = [[0, 1, 1, 0], 
         [1, 0, 1, 0], 
         [1, 1, 0, 1], 
         [0, 0, 1, 1]]

Expected output: 2 0 1 3 ([2, 3, 1, 2], [3, 4, 2, 3]) where the first array indicates discovery time and the second indicates finish time (by index).

Taken that idea I implemented the DFS using Stack:

def dfs_stack(graph, source):
    """Depth-first search algorithm using stack

    This function computes depth-first search and prints the nodes as it travels

    Arguments:
        graph: List[List[int]], adjacency matrix of the graph
        source: int, the starting point
    Returns:
        None
    """
    visited = [False] * len(graph)
    dfs_visit(graph, source, visited)
    return time_start, time_finish

def dfs_visit(graph, source, visited):
    stack = []
    stack.append(source)

    while (len(stack)):
        s = stack[-1]
        stack.pop()
        if not visited[s]:
            print(s, end = " ")
            visited[s] = True
        for idx, val in enumerate(graph[s]):
            if (not visited[idx]) and val != 0:
                stack.append(idx)

I try to put time += 1; time_start[s] = ... to calculate these time but it outputs weird result. Where should I put the time counter correctly?


Solution

  • First a few remarks about your code:

    Concerning the recursive solution

    The times that are logged are somewhat confusing, as you have duplicate timestamps (e.g. 3). This is because the increments you make to time are not fed back to the caller, which has its own time variable instance. I would make time a non local variable, so that it keeps incrementing.

    So I would change that to:

    def dfs(graph, source):
        def dfs_visit(graph, source, visited, time_start, time_finish):
            nonlocal time
            time += 1
            time_start[source] = time
            visited[source] = True
            print(source, end = " ")
            for i, val in enumerate(graph[source]):
                if not visited[i] and val != 0:
                    dfs_visit(graph, i, visited, time_start, time_finish)
            time += 1
            time_finish[source] = time
    
        visited = [False] * len(graph)
        time_start = [0] * len(graph)
        time_finish = [0] * len(graph)
        time = 0
        dfs_visit(graph, source, visited, time_start, time_finish)
        return time_start, time_finish
    

    Now the output of print(dfs(graph, 2)) will be:

    2 0 1 3 ([2, 3, 1, 6], [5, 4, 8, 7])
    

    This makes more sense to me, but maybe I misunderstood what you intended to do with time.

    Concerning the iterative solution

    • s = stack[-1] followed by stack.pop() can really be written as s = stack.pop()

    • You are pushing all children of a node unto the stack before processing their children. This means that actually the depth-first traversal will visit children from last-to-first, while your recursive implementation visits them from first-to-last.

    Logging finishing times

    Now to the core of your question. If you want to register the finishing time of a visit, you'll need to leave a trace of the node on the stack, and only remove it from that stack when all its children have been processed; not earlier.

    One way to achieve that, is to store on the stack which was the last neighbor that was visited from the node. So you would store (node, neighbor) tuples. If no next node had been visited yet from that node, then the initial value for neighbor could be -1.

    Here is how that would work:

    def dfs_stack(graph, source):
        visited = [False] * len(graph)
        time_start = [0] * len(graph)
        time_finish = [0] * len(graph)
        time = 0
        stack = [(source, -1)]
        while stack:
            node, neighbor = stack.pop()
            if neighbor == -1:
                if visited[node]:
                    continue
                visited[node] = True
                print(node, end = " ")
                time += 1
                time_start[node] = time
            try:
                neighbor = graph[node].index(1, neighbor + 1)
                stack.append((node, neighbor))
                if not visited[neighbor]:
                    stack.append((neighbor, -1))
            except ValueError:  # no more neighbors...
                time += 1
                time_finish[node] = time
    

    If we call this with print(dfs_stack(graph, 2)) we also get:

    2 0 1 3 ([2, 3, 1, 6], [5, 4, 8, 7])