54. Lowest Common Ancestor
Problem
Ties to the escalation-hierarchy framing already used in problems 10 and 45: given an org-chart tree of teams and two specific team nodes, find their lowest common ancestor — the deepest team that has both as descendants, where a team counts as its own descendant.
p and q are guaranteed to exist in the tree and are given as
actual node references, not values to search for by equality.
Source: src/54_lowest_common_ancestor.py
class TreeNode:
def __init__(self, val=0, left=None, right=None): ...
def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: ...
# tree: 3
# / \
# 5 1
# / \ / \
# 6 2 0 8
lowest_common_ancestor(root, root.left, root.right).val # -> 3
lowest_common_ancestor(root, root.left, root.left.left).val # -> 5
Step-by-Step Approach
- Do a recursive DFS where the return value itself carries the answer information:
Nonemeans "neitherpnorqwas found in this subtree," and a node means either "this node ISporq" or "this node IS the LCA." - Base case: if the current node is
None, or it ISp, or it ISq, return it immediately without recursing further. Stopping atp/qis safe because the problem guarantees a node counts as its own ancestor — there's no need to look inside a subtree rooted atporqfor the other one. - Recurse into the left and right subtrees, collecting whatever each side reports back.
- If BOTH sides returned something non-
None, that meanspandqwere found in different subtrees of the current node — this node is exactly the point where their paths from the root split apart, so it IS the LCA. Return the current node. - If only ONE side returned something non-
None, that side already found eitherp,q, or the LCA — propagate that value up unchanged, since the current node isn't where the split happens. - If neither side found anything, return
None— this subtree contains neither target.
The elegance here is that the same recursive call answers two different questions depending on where it's called from: at the true LCA, "both children found something" identifies the split point; everywhere else, the found node is simply relayed upward untouched. No parent pointers, no explicit path storage — the recursion's return values alone reconstruct the answer. Runtime is O(n), since in the worst case the DFS visits every node once.
Reference solution
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def lowest_common_ancestor(root, p, q):
# recursive DFS where the return value carries meaning, O(n) time/O(h) space
if root is None or root is p or root is q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
# p and q were found in DIFFERENT subtrees -- this node is where their paths split
return root
# only one side found something -- propagate it up unchanged
return left if left else right
Key Functions & Tricks
root is p or root is q— identity check (not value equality), and short-circuits recursion the instant either target is reached.- Return value doubles as signal —
Nonemeans "not found here," a node means "foundp/q/the LCA," with no separate boolean flags. if left and right— both sides non-Noneis exactly the condition that identifies the split point (the LCA).left if left else right— propagates whichever single side found something, unchanged, when the current node isn't the split point.- No parent pointers or path lists — the whole answer falls out of what recursive calls return, not extra bookkeeping.
How to Recognize This Pattern
Signal words: "lowest/least common ancestor," "deepest shared ancestor of two nodes," "closest common manager in an org chart." The tell is needing to find where two specific nodes' paths-from-root diverge, in a tree with no parent pointers available. A common pitfall is assuming this is a binary search tree and using value comparisons to steer left/right — that only works for BSTs; the general binary-tree version shown here makes no ordering assumption and must search both subtrees unconditionally. Variations: LCA in a BST (values let you skip one whole subtree at each step, avoiding a full search), or LCA with explicit parent pointers (walk both nodes' ancestor chains and find where they intersect, similar to detecting an intersection point in two linked lists).