What is the iterative (non-recursive) version of the LCA (Lower Common Ancestor)

17

In Theory of Graphs, there is a concept called LCA Lower Ancestor - Ancestral ), where given a pair of nodes from a tree, one must find the nearest "parent" (ancestor) of these two nodes.

An example tree is:

IdidarecursiveJavaimplementationbasedonthealgorithmfound.YoucaneditandrunthecodeI'vedeployedin ideone .

The question is: is there an iterative version of this algorithm?

Note that I'm not necessarily looking for a Java implementation, but at least a pseudo-code.

Update: In general, statements of this type of problem do not include information on the "parent" of each node, nor information on the "depth" level. The basis for implementation is the following class:

class Node {
    List<Node> children;
    Integer id;
}
    
asked by anonymous 27.12.2013 / 19:23

4 answers

3

Wikipedia contains a page on Tree Traversal that includes pseudo-codes for iterate over binary tree using cells. For example, this is the Pre-order Traversal :

iterativePreorder(node)
  parentStack = empty stack
  while (not parentStack.isEmpty() or node ≠ null)
    if (node ≠ null)
      visit(node) #1
      parentStack.push(node) #2
      node = node.left
    else
      node = parentStack.pop() #3
      node = node.right

I wrote down three points (# 1, # 2, # 3), which I will comment on later.

However, to solve the problem in question (LCA), not limiting to two descendants per node (binary tree), some adjustments are necessary:

  • An auxiliary stack to store the current child being visited at each level as the algorithm descends into the tree. Unlike a binary tree, where it is enough to go through the left node and then the right node to go through a node with n children we need a counter. And since each child can have m children, then there should be a counter for each level of the tree.
  • A second helper stack to store the path to the first node found. As one of the goals of the algorithm is to find two nodes, we must store the path to the first node and continue until we find the second.
  • A pseudo-code to find the ancestor closest to the nodes p and q , given the root node , looks like this:

    findClosestCommonAncestor(node, p, q)
      parentStack = empty stack
      childIndexStack = empty stack
      firstNodePath = null
      while (not parentStack.isEmpty() or node ≠ null)
        if (node ≠ null)
    
          #1
          if (node == p || node == q)
            if (firstNodePath ≠ null)
              parentStack.add(node)
              int n = min(parentStack.length, firstNodePath.length)
              for i = (n - 1)..0
                if (parentStack(i) == firstNodePath(i))
                  return parentStack(i)
              return null
            else
              firstNodePath = copy parentStack
              firstNodePath.push(node)
    
          #2
          if (not empty node.children)
            parentStack.push(node)
            childIndexStack.push(0)
            node = node.children(0)
          else
            node = null
    
        else
    
          #3
          node = parentStack.peek()
          i = childIndexStack.pop() + 1
          if (i >= node.children.length)
            node = null
            parentStack.pop()
          else
            node = node.children(i)
            childIndexStack.push(i)
    

    It certainly got more complex, but the concept is basically the same as the previous one. Note that I also marked in this algorithm three points, because they are analogous to the previous one. Let's see:

    • # 1 This is the block where the value of the current node is processed. The visit(node) of the first algorithm was replaced by a block that checks whether one of the nodes was found. If you have found the first node it saves the current stack. If you have found both, it compares the stacks, item by item, by looking for the nearest parent.
    • # 2 The initial algorithm adds the current node in the stack and advances to the child on the left. The second algorithm generalizes to n children advancing to the first child (0).
    • # 3 The initial algorithm pops up a node and advances to the right child. The second algorithm generalizes by advancing to the next child (previous + 1).

    The Java code looks like this:

    class Node {
        List<Node> children = new ArrayList<Node>();
        Integer id;
        Node(Integer id) {
            this.id = id;
        }
    }
    Node findClosestCommonAncestor(Node node, Node p, Node q) {
    
        Stack<Node> parentStack = new Stack<Node>();
        Stack<Integer> childIndexStack = new Stack<Integer>();
        Stack<Node> firstNodePath = null;
        while (!parentStack.empty() || node != null) {
    
            if (node != null) {
    
                if (node == p || node == q) {
    
                    if (firstNodePath != null) {
                        parentStack.add(node);
                        int n = Math.min(parentStack.size(), firstNodePath.size());
                        for (int i = n - 1; i >= 0; i--) {
                            if (parentStack.get(i) == firstNodePath.get(i)) {
                                return parentStack.get(i); 
                            }
                        }
                        return null;
    
                    } else {
                        firstNodePath = new Stack<Node>();
                        firstNodePath.setSize(parentStack.size());
                        Collections.copy(firstNodePath, parentStack);
                        firstNodePath.push(node);
                    }
    
                }
    
                if (!node.children.isEmpty()) {
                    parentStack.push(node);
                    childIndexStack.push(0);
                    node = node.children.get(0);
                } else {
                    node = null;
                }
    
            } else {
    
                node = parentStack.peek();
                Integer i = childIndexStack.pop() + 1;
                if (i >= node.children.size()) {
                    node = null;
                    parentStack.pop();
                } else {
                    node = node.children.get(i);
                    childIndexStack.push(i);
                }
    
            }
    
        }
        return null;
    
    }
    

    The full version of the Java code is available for editing and testing at ideone.com .

    Update

    In contrast to the statement proposed in the question, if there were information about the parent node and not the children, a much more efficient algorithm could be proposed.

    Suppose that two nodes P and Q are given. This algorithm only needs:

  • Store P and their parents the nodes in a set C
  • Go through the parents of Q , starting from the element itself and returning the first element Q(i) that is in the C set.
  • Java code:

    static class Node {
        Node parent;
        Integer id;
        Node(Integer id, Node parent) {
            this.id = id;
            this.parent = parent;
        }
        public String toString() {
            return id.toString();
        }
    }
    
    static Node findClosestCommonAncestor(Node p, Node q) {
        if (p == null || q == null) return null;
    
        //guarda os pais do nó P, incluindo o próprio
        Set<Node> parentsOfP = new HashSet<Node>();
        while (p != null) {
            parentsOfP.add(p);
            p = p.parent;
        }
    
        //procura o primeiro pai de Q que está entre os pais de P
        while (q != null) {
            if (parentsOfP.contains(q)) {
                return q;
            }
            q = q.parent;
        }
        return null;// not in the same tree
    }
    

    Code on IdeOne

        
    02.01.2014 / 12:07
    7

    If each node in your tree has information about the "depth" (i.e. the number of nodes between it and the root) then you can replace each node by its parent until both are the same node:

    entradas a, b
    profundidade_minima <- min(profundidade(a), profundidade(b))
    enquanto profundidade(a) > profundidade_minima:
        a <- pai(a)
    fim
    enquanto profundidade(b) > profundidade_minima:
        b <- pai(b)
    fim
    enquanto a != b:
        a <- pai(a)
        b <- pai(b)
    fim
    saída a
    

    If you do not know the depth a priori , it is easy to get it iteratively:

    entradas a, raiz
    profundidade <- 0
    enquanto a != raiz:
        profundidade <- profundidade + 1
        a <- pai(a)
    fim
    saída profundidade
    

    Note: this answer was based on the assumption that each node has a reference to its parent (assumption already rectified by editing the question).

    Update: I may be wrong, but I believe there is no iterative solution for the case where a node has no reference to its parent (except, of course, the general technique of simulating recursion through an explicit stack). The reason is that the nodes to be compared can be anywhere in the tree, and without any indication of where it is necessary to search the entire tree. A binary tree (the simplest one) will potentially have 2^n nodes, where n is its maximum depth.

    No matter which way you conduct a search, if the x node has been visited and it has 2 or more children, you will have to "remember" that your other children still have to visit before starting the search. search for one of them. That is, children not yet visited will have to be placed in a pile, queue or similar. The method may vary, but would still be equivalent to a recursive solution (in terms of performance and memory usage).

    From there you have several options: map each node to your parent (and use a solution like the one I proposed above) and its depth, map each node to its path to the root, search for common ancestor simultaneously with search for both nodes, etc.

        
    27.12.2013 / 19:41
    5

    Very good answer utluiz! But if I may, in my opinion an algorithm is characterized by its efficiency, memory consumption and accuracy for each case of the problem being solved. As your algorithm has exactly the same features as the recursion implementation I would say that in essence they are the same algorithm!

    If you print each node you notice the algorithm will visit the nodes in this order:

    Thisisexactlythesameorderinwhicharecursivealgorithmwouldvisiteachnode.Plusbyusingastackitspendsproportionallythesameamountofmemory!ThetwoalgorithmshaveO(n)fortheworstcase,andwillvisitexactlythesamenumberofnodesforeachcase!

    Iwillproposeatrulynon-recursivealgorithm:

    staticNodefindClosestCommonAncestor(NodeancestralComum,Nodep,Nodeq){Queue<Node>fila=newArrayBlockingQueue<Node>(100);while(true){//Adicionartodososfilhosdoancestralcomumobtidoateagoranafilafila.clear();for(Nodee:ancestralComum.children){fila.add(e);fila.add(e);}//VaipassandoosancestraisatedescobrirosnodeseseusancestraisNodep_ancestral=null,q_ancestral=null;while(p_ancestral==null||q_ancestral==null){Nodee=fila.remove();Nodea=fila.remove();for(Nodefilho:e.children){fila.add(filho);fila.add(a);}if(e==p)p_ancestral=a;if(e==q)q_ancestral=a;}//Condicoesparaterminooucontinuacaodoalgoritmoif(p_ancestral==q_ancestral)ancestralComum=p_ancestral;elsebreak;if(p==ancestralComum||q==ancestralComum)break;}returnancestralComum;}

    Thefullcodecanbeviewedandrunon Ideone .

    Notice that he visits the nodes in a completely different way:

    It uses a queue instead of a stack, and is implemented much more efficiently with iterations than with recursion!

    Its version uses deep search and recursive methods are very efficient in this type of algorithm. This version uses search width which is much better implemented with iterative methods.

    Where n is the number of nodes in the tree:

    Search in depth (your algorithm)

    • Complexity O (n), in the worst case visit all nodes in the tree
    • The worst case of memory spent is proportional to the depth of the tree
    • If both nodes are deep in the tree it will be very efficient
    • If they are very close to the root it can be very inefficient

    Width (my) search

    • Complexity O (n ^ 2), in the worst case visit n ^ 2 nodes (visit the same ones repeatedly)
    • The worst case of memory spent is proportional to the maximum width of the tree
    • If both nodes are deep in the tree it will be pretty bad
    • If they are very close to the root it will be very efficient

    There is a version of this algorithm using binary search that has complexity O (n * logn) and spends the same amount of memory, which is much better than O (n ^ 2). There is also a version using dynamic programming that is O (n), but it spends a lot more memory. On occasions when the tree is too deep and the maximum width is small, a recursive method or using a stack can spend a lot of memory while an iterative one does not. And if the graph has a large depth and the desired nodes are on the surface the search in width will be much more efficient!

        
    12.05.2016 / 02:42
    1

    Using the Stack concept, in this link you can find an example of algorithm and implementation.

    In the example, we see how to traverse the tree without using recursion, so the shortest distance can be obtained by adding a step in the algorithm

        
    27.12.2013 / 19:43