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

Asked

Viewed 2,247 times

18

In graph theory, there is a concept called LCA (Lower Common Ancestor - Nearest common ancestor), where given a pair of nodes of a tree, one must find the nearest "father" (ancestor) of these two nodes.

An example of a tree is:

Árvore

I made a recursive implementation in Java based on the algorithm found at this link. You can edit and execute the code I implemented on ideone.

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

Note that I am 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 the implementation is the following class:

class Node {
    List<Node> children;
    Integer id;
}
  • 1

    Do your nodes have references to the father? This is not clear in the question (I did not read the reference in the OS). If not, my answer below does not apply...

  • 1

    Unfortunately the problem at hand usually does not have this information. Obviously the algorithm could have an additional step to go through the tree and store this information. I’ve actually solved it that way, that is, dividing it into steps, but I’m looking for a more refined algorithm.

4 answers

8

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 with your 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 don’t 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 reference to its father (assumption already rectified by the edition in the question).


Updating: I may be mistaken, 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 by means of 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 ends up being necessary to search the whole tree. A binary tree (the simplest) will potentially have 2^n us, where n is your maximum depth.

No matter which way you do a search, if the knot x was visited and he has 2 or more children, you will have to "remember" that you still have to visit your other children before starting the search for one of them. In other words, children not yet visited will have to be placed in a pile, queue or similar. The method may vary, but it 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 to search for both nodes, etc.

  • That’s a good answer, since I didn’t put any more restrictions on the problem. However, according to my code, in general the statements of this type of problem include neither the depth information nor the "parent node".

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. Since 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 visited note that the algorithm will visit the nodes in this order:

inserir a descrição da imagem aqui

This is exactly the same order in which a recursive algorithm would visit each node. Also by using a stack it consumes proportionally the same amount of memory! The two algorithms have O(n) for the worst case, and will visit exactly the same number of nodes for each case!

I’m going to propose a truly nonrecursive algorithm:

static Node findClosestCommonAncestor(Node ancestralComum, Node p, Node q) {
    Queue<Node> fila = new ArrayBlockingQueue<Node>(100);
    while(true){
        // Adicionar todos os filhos do ancestral comum obtido ate agora na fila
        fila.clear();
        for( Node e: ancestralComum.children ){
            fila.add(e);
            fila.add(e);
        }
        // Vai passando os ancestrais ate descobrir os nodes e seus ancestrais
        Node p_ancestral=null, q_ancestral=null;
        while(p_ancestral == null || q_ancestral == null){
            Node e = fila.remove();
            Node a = fila.remove();
            for( Node filho : e.children ){
                fila.add(filho);
                fila.add(a);
            }
            if(e == p)
                p_ancestral = a;
            if(e == q)
                q_ancestral = a;    
        }
        // Condicoes para termino ou continuacao do algoritmo
        if(p_ancestral == q_ancestral)
            ancestralComum = p_ancestral;
        else
            break;
        if(p == ancestralComum || q == ancestralComum)
            break;
    }
    return ancestralComum;
}

The complete code can be viewed and executed on Ideone.

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

inserir a descrição da imagem aqui

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

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

Where n is the number of nodes of the tree:

Deep Search (your algorithm)

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

Search in width (my)

  • Complexity O(n 2), worst case visit n 2 knots (visit the same repeatedly)
  • Worst case spent memory is proportional to maximum tree width
  • If the two knots are deep in the tree it will be very bad
  • If they are close to the root it will be very efficient

There is a version of this algorithm using binary search that has O(n*logn) complexity and uses 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 uses a lot more memory. At times when the tree is very deep and the maximum width is small, a recursive or stack method can spend a lot of memory while an iterative 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!

  • 3

    +1 for sound complexity analysis.

4


Wikipedia contains a page about Tree Traversal which includes pseudo-codes to iterate over binary tree using stacks. For example, this is from 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 being limited to two descendants per node (binary tree), some adjustments are necessary:

  1. An auxiliary stack to store the current child being visited at each level that the algorithm goes down in the tree. Different from a binary tree, where you just go through the left node and then the right node, to go through a node with n children we need an accountant. And how each child can have m children, then there should be a counter for each level of the tree.
  2. A second auxiliary battery to store the way up to the first node found. As one of the objectives of the algorithm is to find two nodes, we must store the path to the first and continue until we find the second.

A pseudo-code to find the closest ancestor to the nodes p and q, given the root node, was 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 became more complex, but the concept is basically the same as the previous one. Note that I also marked in this algorithm three points, as they are analogous to the previous one. Let’s see:

  • #1 This is the block where the value of the current node is processed. O 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 found both, you compare the stacks, item by item, looking for the nearest parent.
  • #2 The initial algorithm adds the current node on the stack and advances to the left child. The second algorithm generalizes to n Children advancing to the first child (0).
  • #3 The initial algorithm pops a node and advances to the right child. The second algorithm generalizes by advancing to the next child (previous + 1).

The code in Java looked 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 Java code is available for editing and testing on ideone..


Updating

Contrary to the statement proposed in the question, if there was information of the parent node and not of the children, a much more efficient algorithm could be proposed.

Suppose you are given two nodes P and Q. This algorithm just needs:

  1. Store P and their parents us in one set C
  2. Walk through the parents of Q, starting from the element itself and returning the first element Q(i) that is in the set C.

Code in Java:

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 in Ideone

  • I put my answer as right because I managed to get the expected result according to the parameters of the question.

1

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

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

  • Walking through a tree is "easy". The problem in question is somewhat more complex.

  • +1 by interesting link.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.