Youngest Common Ancestor
Youngest Common Ancestor
Category
Graphs
Difficulty
Medium
Problem Statement
Given three inputs: the top ancestor of a tree (not necessarily binary), and two descendant nodes, find the youngest (deepest, lowest) common ancestor of the two descendant nodes. Each node has a reference to its parent (an ancestor property). The youngest common ancestor is the deepest node that is an ancestor of both given nodes (a node is considered an ancestor of itself).
Intuition
If both nodes are at the same depth, we can walk both pointers up simultaneously until they meet. If they are at different depths, we first bring the deeper node up to the same depth as the shallower one, then walk both up in tandem. The node where the two pointers converge is the youngest common ancestor.
This is analogous to the "intersection of two linked lists" problem, where the lists merge at some common ancestor.
Approach
- Compute the depth of both descendant nodes by counting steps to the top ancestor.
- Determine which node is deeper and by how much.
- Advance the deeper node upward by the depth difference.
- Now both nodes are at the same depth. Walk both upward simultaneously until they point to the same node.
- Return that node.
Pseudocode
function getYoungestCommonAncestor(topAncestor, descendantOne, descendantTwo):
depthOne = getDepth(descendantOne, topAncestor)
depthTwo = getDepth(descendantTwo, topAncestor)
if depthOne > depthTwo:
return walkUp(descendantOne, descendantTwo, depthOne - depthTwo)
else:
return walkUp(descendantTwo, descendantOne, depthTwo - depthOne)
function getDepth(node, topAncestor):
depth = 0
while node != topAncestor:
node = node.ancestor
depth += 1
return depth
function walkUp(lowerNode, higherNode, diff):
while diff > 0:
lowerNode = lowerNode.ancestor
diff -= 1
while lowerNode != higherNode:
lowerNode = lowerNode.ancestor
higherNode = higherNode.ancestor
return lowerNode
Time & Space Complexity
- Time: O(d), where d is the depth of the deeper node. We traverse up the tree at most twice.
- Space: O(1). Only pointer variables are used.
Key Insights
- The parent pointer makes this much simpler than LCA in a tree without parent pointers (which requires preprocessing or Euler tour techniques).
- Equalizing depths first is the key step; without it, the two pointers would never synchronize.
- This algorithm also works when one node is an ancestor of the other.
- An alternative approach stores all ancestors of one node in a set, then walks up from the other node checking for membership. This uses O(d) space.
Edge Cases
- One descendant is an ancestor of the other: the ancestor node is the youngest common ancestor.
- Both descendants are the same node: that node itself is the answer.
- Both descendants are direct children of the top ancestor: the top ancestor is the answer.
- One or both descendants are the top ancestor itself.
- Deeply unbalanced tree where one path is much longer than the other.