4

I'm trying to create depth-first algorithm that assigns finishing times (the time when a vertex can no longer be expanded) which are used for things like Kosaraju's algorithm. I was able to create a recursive version of DFS fairly easily, but I'm having a hard time converting it to an iterative version.

I'm using an adjacency list to represent the graph: a dict of vertices. For example, the input graph {1: [0, 4], 2: [1, 5], 3: [1], 4: [1, 3], 5: [2, 4], 6: [3, 4, 7, 8], 7: [5, 6], 8: [9], 9: [6, 11], 10: [9], 11: [10]} represents edges (1,0), (1,4), (2,1), (2,5), etc. The following is the implementation of an iterative DFS that uses a simple stack (LIFO), but it doesn't compute finishing times. One of the key problems I faced was that since the vertices are popped, there is no way for the algorithm to trace back its path once a vertex has been fully expanded (unlike in recursion). How do I fix this?

def dfs(graph, vertex, finish, explored):
    global count

    stack = []
    stack.append(vertex)

    while len(stack) != 0:
        vertex = stack.pop()

        if explored[vertex] == False:
            explored[vertex] = True

            #add all outgoing edges to stack:
            if vertex in graph:                 #check if key exists in hash -- since not all vertices have outgoing edges
                for v in graph[vertex]:
                    stack.append(v)

        #this doesn't assign finishing times, it assigns the times when vertices are discovered:                                            
        #finish[count] = vertex
        #count += 1

N.b. there is also an outer loop that complements DFS -- though, I don't think the problem lies there:

#outer loop:
for vertex in range(size, 0, -1):
    if explored[vertex] == False:
        dfs(hGraph, vertex, finish, explored)
Eyeofpie
  • 330
  • 2
  • 13

3 Answers3

10

Think of your stack as a stack of tasks, not vertices. There are two types of task you need to do. You need to expand vertexes, and you need to add finishing times.

When you go to expand a vertex, you first add the task of computing a finishing time, then add expanding every child vertex.

When you go to add a finishing time, you can do so knowing that expansion finished.

btilly
  • 43,296
  • 3
  • 59
  • 88
  • Great explanation. After reading, my question then became "How do I know if I should be expanding a vertex or adding a finishing time?" It became clear that it depends on the search status of the node popped off the stack at the beginning of the loop. If the node was only explored then vertices need to be expanded and the node marked as processed. If the node was processed then a finishing time needs to be added. – Dylan Nissley Aug 01 '15 at 18:49
  • Expansion finished is not the way to find the finishing time. When after expansion the child nodes are finished, in that case we know the parent is finished. So the finishing time can be recorded when (1) there is no outward edges for the node, (2) when the parent of the traversing node changes (3) when the stack finishes. In the last two cases the parent hits the finish time(rather than the current node). – KRoy May 10 '16 at 17:48
  • 2
    @shuva The algorithm as I described it works perfectly. What you are describing might be able to be made to work, but will require far more state to be kept around. – btilly May 10 '16 at 19:21
  • @btilly you are right, we can keep the task in the stack instead of the vertex. This is the idea to stack up the flow of the algorithm. It will simulate the same recursive states of DFS, thereby implicating a correct solution. By the way I implemented my way of thinking into working code. It is added as answer. – KRoy May 15 '16 at 21:46
1

Here is a working solution that uses two stacks during the iterative subroutine. The array traceBack holds the vertices that have been explored and is associated with complementary 2D-array, stack, that holds arrays of edges that have yet to be explored. These two arrays are linked; when we add an element to traceBack we also add to stack (same with popping elements).

count = 0
def outerLoop(hGraph, N):
    explored    = [False for iii in range(N+1)]
    finish      = {}

    for vertex in range(N, -1, -1):
        if explored[vertex] == False:
            dfs(vertex, hGraph, explored, finish)

    return finish


def dfs(vertex, graph, explored, finish):
    global count
    stack       = [[]]                              #stack contains the possible edges to explore
    traceBack   = []
    traceBack.append(vertex)

    while len(stack) > 0:
        explored[vertex] = True

        try:
            for n in graph[vertex]:
                if explored[n] == False:
                    if n not in stack[-1]:          #to prevent double adding (when we backtrack to previous vertex)
                        stack[-1].append(n)
                else:
                    if n in stack[-1]:              #make sure num exists in array before removing
                        stack[-1].remove(n)             
        except IndexError: pass

        if len(stack[-1]) == 0:                     #if current stack is empty then there are no outgoing edges:
            finish[count] = traceBack.pop()         #thus, we can add finishing times
            count += 1

            if len(traceBack) > 0:                  #to prevent popping empty array
                vertex = traceBack[-1]
            stack.pop()
        else:
            vertex = stack[-1][-1]                  #pick last element in top of stack to be new vertex

            stack.append([])
            traceBack.append(vertex)
Eyeofpie
  • 330
  • 2
  • 13
0

Here is a way. Every time we face the following condition, we do a callback or we mark the time,

  • The node has no outgoing edge(no way).
  • When the parent of the traversing node is different from last parent(parent change). In that case we finish the last parent.
  • When we reach at the end of the stack(tree end). We finish the last parent.

Here is the code,

var dfs_with_finishing_time = function(graph, start, cb) {
    var explored = [];
    var parent = [];
    var i = 0;
    for(i = 0; i < graph.length; i++) {
            if(i in explored)
                    continue;
            explored[i] = 1;
            var stack = [i];
            parent[i] = -1;
            var last_parent = -1;
            while(stack.length) {
                    var u = stack.pop();
                    var k = 0;
                    var no_way = true;
                    for(k = 0; k < graph.length; k++) {
                            if(k in explored)
                                    continue;
                            if(!graph[u][k])
                                    continue;
                            stack.push(k);
                            explored[k] = 1;
                            parent[k] = u;
                            no_way = false;
                    }
                    if(no_way) {
                            cb(null, u+1); // no way, reversed post-ordering (finishing time)
                    }
                    if(last_parent != parent[u] && last_parent != -1) {
                            cb(null, last_parent+1); // parent change, reversed post-ordering (finishing time)
                    }
                    last_parent = parent[u];
            }
            if(last_parent != -1) {
                    cb(null, last_parent+1); // tree end, reversed post-ordering (finishing time)
            }
    }
}
KRoy
  • 1,290
  • 14
  • 10