Lowest Common Ancestor - Depth First Search / Advanced

https://algo.monster/problems/lowest_common_ancestor

simple solution:
public static Node lca(Node root, Node node1, Node node2) {
if (root == null) return null;
if (node1.val > root.val && node2.val > root.val) return lca(root.right, node1, node2);
else if (node1.val < root.val && node2.val < root.val) return lca(root.left, node1, node2);
else return root;
}

All the test cases are Binary Search Trees. A simple code comparing the nodes can solve this in O(log n). Please add some Binary tree test cases.

1 Like

This question could use some clarification around expected behavior if the value for v or w isn’t in the tree. Consider a tree 6 4 x 5 x x 8 x x with nodes 3 and 4. The python solution outputs 4, but 3 isn’t a node in the tree. It seems like either the code should return None if the nodes don’t exist, or it should be stated in the problem that we should assume both nodes exist in the tree. Am I misunderstanding?

Yeah - I have this issue as well, it must be assuming that the nodes exist in the tree - I was stuck on this for a while =
But on the other hand, if none of the nodes exists, the code will, correctly, return null - so this assumption is not consistent.

It only returns null if none of the nodes are in the tree, if any one of them are in there, it’ll return that one.

Not the most efficient, but this works and does not assume that the tree contains both of the nodes

def has_node(root: Node, to_search_for: Node) → bool:
if root is None:
return None
if root.val == to_search_for.val:
return True
is_in_left = has_node(root.left, to_search_for)
is_in_right = has_node(root.right, to_search_for)
return is_in_left or is_in_right

def lca(root: Node, node1: Node, node2:Node) → Node:
if root is None:
return None

#if we are node 1 and node 2 exists on left or right, the we are the LCA
if root.val == node1.val:
    if has_node(root.left, node2):
        return root
    if has_node(root.right, node2):
        return root

#if we are node 2 and node 1 exists on left or right, then we are the LCA
if root.val == node2.val:
    if has_node(root.left, node1):
        return root
    if has_node(root.right, node1):
        return root

#See if we find node1 on the left
node1_on_left = has_node(root.left, node1)
node1_on_right = has_node(root.right, node1)
#If we do not have a node1
if not (node1_on_left or node1_on_right):
    return None

node2_on_left = has_node(root.left, node2)
node2_on_right = has_node(root.right, node2)
#If we do not have a node2
if not (node2_on_left or node2_on_right):
    return None

#we are LCA if node1 and node two exist on opposite sides
if (node1_on_left and node2_on_right) or (node1_on_right and node2_on_left):
    return root

#we are not the LCA, try to find it on the left
lca_on_the_left = lca(root.left, node1, node2)
if lca_on_the_left is not None:
    return lca_on_the_left

#didn't find it on the left, try on the right
lca_on_the_right = lca(root.right, node1, node2)
if lca_on_the_right is not None:
    return lca_on_the_right

#We do not have an LCA under us
return None

It would be nice if there was type declaration in the function signature. I just spent 10 minutes trying to figure out where my code went wrong, when all I had to do was remove .value from if root.value == node1 or root.value == node2

would also have been nice to know it’s a binary search tree, not just a binary tree. Video solution and associated LC problem are both BST and guarantee solutions.

For the Javascript solution, if (root === node1 || root === node2) return root; how does this make sense? so we aren’t exploring any deeper in the current node of our subtree if that node is eqal to node1 or node 2? doesn’t this prevent us from observing a case 2 scenario?

Guys, we are re-writing this article with a better explanation very soon. Stay tuned. Thanks :slight_smile:

Here’s my approach:

soln = None
def lca(root, node1, node2):
def dfs(root, node1, node2):
global soln
if root is None:
return 0

    left = dfs(root.left, node1, node2)
    right = dfs(root.right, node1, node2)
    mid = 1 if root.val in [node1.val, node2.val] else 0
    
    if left + right + mid >= 2:
        soln = root
        
    return 1 if (mid + left + right > 0) else 0

dfs(root, node1, node2)
return soln

soln = None
def lca(root, node1, node2):
def dfs(root, node1, node2):
global soln
if root is None:
return 0

    left = dfs(root.left, node1, node2)
    right = dfs(root.right, node1, node2)
    mid = 1 if root.val in [node1.val, node2.val] else 0
    
    if left + right + mid >= 2:
        soln = root
        
    return 1 if (mid + left + right > 0) else 0

dfs(root, node1, node2)
return soln

How do we format code before we post?

Not as concise, but clearer to understand IMO

def lca(root, node1, node2):
    #     update all nodes with parents; find p and q, stop when both are found
    def add_parent(node, parent, p, q):
        if not node: return p, q
        node.parent = parent
        if node == node1:
            p = node
        if node == node2:
            q = node
        if p is None or q is None:  # keep DFS going only when either of the nodes is not found
            p, q = add_parent(node.left, node, p, q)
            p, q = add_parent(node.right, node, p, q)
        return p, q
    
    #     find lowest shared parent - helper method to build a list of nodes from root to node
    def path_to_root(node):
        res = []
        while True:
            res.append(node)
            if node.parent == 'root':
                break
            node = node.parent
        return res[::-1]
        
    p, q = add_parent(root, 'root', None, None)
    if p is None or q is None: # at least one node is not found
        return None
    p_path = path_to_root(p)
    q_path = path_to_root(q)
    #     given two lists (paths from root), return their righmost shared element
    res = [l for l, r in zip(p_path, q_path) if l == r][-1]
    
    return res

I had 2 ways to do this… one pretty much the same as the answer provided and one using path building

public static TreeNode lca(TreeNode root, TreeNode node1, TreeNode node2, int type) {
switch (type) {
case 1:
return lcaUsingChildKnowledge(root, node1, node2);
default:
return lcaUsingPath(root, node1, node2);
}
}

public static TreeNode<Integer> lcaUsingChildKnowledge(TreeNode<Integer>  root, TreeNode<Integer>  node1, TreeNode<Integer>  node2){
    // If either (node1 or node2) is in my left side and the other is in my right side then return myself as I'm the lca
    // If my kids pass me up a node them that means either that node is node1/node2 (or it's the lca)... so I should pass that back up
    // If I'm either of the nodes... then I should return myself and my ancestor can check its other subtree for presence of the other
    // otherwise if I'm not the node and I don't have the node in any of my kids... I should return null
    // predicates
    // I'm a node, I'm the lca, I'm neither
    // my left child is a node, is the lca, is neither
    // my right child is a node, is the lca, is either

    TreeNode<Integer> result = null;
    if(root == null) return result;
    if(root.equals(node1) || root.equals(node2)){
        // I'm a node... I might be the LCA if one of the below comes back !!
        result = root;
    }
    TreeNode<Integer> leftResult = lcaUsingChildKnowledge(root.left, node1, node2);
    if(leftResult != null){
        // my left has a node or is an lca
        if(result == null) result = leftResult;
    }
    TreeNode<Integer> rightResult = lcaUsingChildKnowledge(root.right, node1, node2);
    if(rightResult != null){
        // my right has a node or is an lca
        if(leftResult != null) {
            // we had a match from right and left... so root must be lca
            result = root;
        }
       else if(leftResult == null && result == null){
           // right had a match but nothing else did
            result = rightResult;
        }
    }
    return result;
}

public static TreeNode<Integer>  lcaUsingPath(TreeNode<Integer>  root, TreeNode<Integer>  node1, TreeNode<Integer>  node2) {
    List<TreeNode<Integer>> pathToNode1 = new ArrayList<>(), pathToNode2 = new ArrayList<>();
    findPathToNode(root, node1, pathToNode1);
    findPathToNode(root, node2, pathToNode2);
    int shortestPath = Math.min(pathToNode1.size(), pathToNode2.size());
    if(shortestPath == 0) return null;
    else{
        int i = 0;
        for(; i < shortestPath; i++){
            if(!pathToNode1.get(i).equals(pathToNode2.get(i))) break;
        }
        return pathToNode1.get(i-1);
    }
}

public static boolean findPathToNode(TreeNode<Integer>  node, TreeNode<Integer>  nodeToFind, List<TreeNode<Integer>> listToPopulate){
    if(node == null) return false;
    else if(node.equals(nodeToFind)){
        listToPopulate.add(node);
        return true;
    }
    else {
        listToPopulate.add(node);
        if(findPathToNode(node.left, nodeToFind, listToPopulate) == false && findPathToNode(node.right, nodeToFind, listToPopulate) == false) {
            listToPopulate.remove(listToPopulate.size() - 1);
            return false;
        }
        else return true;
    }
}

cleaned it up some

public static TreeNode lcaUsingChildKnowledge(TreeNode root, TreeNode node1, TreeNode node2){
// If either (node1 or node2) is in my left side and the other is in my right side then return myself as I’m the lca
// If my kids pass me up a node them that means either that node is node1/node2 (or it’s the lca)… so I should pass that back up
// If I’m either of the nodes… then I should return myself and my ancestor can check its other subtree for presence of the other
// otherwise if I’m not the node and I don’t have the node in any of my kids… I should return null
// predicates
// I’m a node, I’m the lca, I’m neither
// my left child is a node, is the lca, is neither
// my right child is a node, is the lca, is either
if(root == null) return null;
if(root.equals(node1) || root.equals(node2)) return root; // I’m either lca or one of the nodes
TreeNode leftResult = lcaUsingChildKnowledge(root.left, node1, node2);
TreeNode rightResult = lcaUsingChildKnowledge(root.right, node1, node2);
if(leftResult != null && rightResult != null) return root; // I’m the lca!
else if(leftResult != null) return leftResult; // one of my left children is a node (my right returned nothing)
else if (rightResult != null) return rightResult; // one of my right children is a node (my right returned nothing)
else return rightResult; // no matches at all
}

public static TreeNode<Integer> lcaUsingChildKnowledge(TreeNode<Integer>  root, TreeNode<Integer>  node1, TreeNode<Integer>  node2){
    // If either (node1 or node2) is in my left side and the other is in my right side then return myself as I'm the lca
    // If my kids pass me up a node them that means either that node is node1/node2 (or it's the lca)... so I should pass that back up
    // If I'm either of the nodes... then I should return myself and my ancestor can check its other subtree for presence of the other
    // otherwise if I'm not the node and I don't have the node in any of my kids... I should return null
    // predicates
    // I'm a node, I'm the lca, I'm neither
    // my left child is a node, is the lca, is neither
    // my right child is a node, is the lca, is either
    if(root == null) return null;
    if(root.equals(node1) || root.equals(node2)) return root; // I'm either lca or one of the nodes
    TreeNode<Integer> leftResult = lcaUsingChildKnowledge(root.left, node1, node2);
    TreeNode<Integer> rightResult = lcaUsingChildKnowledge(root.right, node1, node2);
    if(leftResult != null && rightResult != null) return root; // I'm the lca!
    else if(leftResult != null) return leftResult; // one of my left children is a node (my right returned nothing)
    else if (rightResult != null) return rightResult; // one of my right children is a node (my right returned nothing)
    else return rightResult; // no matches at all
}

Sorry formatting :frowning:

public static TreeNode<Integer> lcaUsingChildKnowledge(TreeNode<Integer>  root, TreeNode<Integer>  node1, TreeNode<Integer>  node2){
    if(root == null) return null;
    if(root.equals(node1) || root.equals(node2)) return root; // I'm either lca or one of the nodes
    TreeNode<Integer> leftResult = lcaUsingChildKnowledge(root.left, node1, node2);
    TreeNode<Integer> rightResult = lcaUsingChildKnowledge(root.right, node1, node2);
    if(leftResult != null && rightResult != null) return root; // I'm the lca!
    else if(leftResult != null) return leftResult; // one of my left children is a node (my right returned nothing)
    else if (rightResult != null) return rightResult; // one of my right children is a node (my left returned nothing)
    else return null; // no matches at all
}

:Facepalm: comment spam… small edit to last 2 lines

Thanks, the explanation is wonderful!