Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
To answer many LCA queries on a fixed rooted tree, precompute `up[node][j]` = the 2^j-th ancestor of node, for j up to log n. Build it with one DFS for depths plus the recurrence `up[v][j] = up[ up[v][j-1] ][j-1]`. To answer LCA(a, b): lift the deeper node up to the other's depth, then lift both in powers of two while their ancestors differ. Preprocessing is O(n log n); each query is O(log n).
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can design O(n log n) preprocessing that makes each query O(log n), and whether you correctly handle the two-phase query: equalize depths, then lift both nodes while ancestors differ.
Where candidates slip
Returning the node where the lifts meet instead of its parent, forgetting the already-an-ancestor case (one node is the LCA), or sizing the up table with too few levels so a deep jump runs off the end.
How to stand out
Derive LOG from n, explain the doubling recurrence in one line, and note that distance and k-th ancestor reuse the same table. Mention the iterative DFS to avoid stack overflow on a tree that is a long chain.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachExplore the full treeCollapse the tree
How to read it:lca(3,4)=1 (siblings' parent); lca(3,5)=0 (different branches meet at the root).
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Baseline — Walk Both Pointers Up to the Root
1Hint 1Explore hintHide hint
First make both nodes the same depth: lift the deeper one parent-by-parent until depths match.
2Hint 2Explore hintHide hint
Then move both up one step at a time in lockstep; the first node where they coincide is the LCA.
3Hint 3Explore hintHide hint
Each query can walk the whole height of the tree — O(n) per query. With many queries that is too slow, which motivates jump pointers.
A · Core idea
The direct method needs only parent pointers and a depth for each node.
Step one — equalize depth. An LCA cannot be deeper than the shallower of the two nodes. So first lift the deeper node up, one parent at a time, until both nodes sit at the same depth.
Step two — climb in lockstep. Now move both nodes up one parent at a time together. As long as they differ, keep going; the moment they point at the same node, that node is the LCA. They are guaranteed to meet no later than the root.
Why it is correct. Once depths are equal, the two upward paths to the root have the same length and converge exactly at the LCA — every node above the LCA is shared, every node below is on a separate branch.
Why it is too slow. Each phase can traverse the entire height of the tree, which is O(n) for a chain. Answering q queries this way is O(q·n) — fine for a handful of queries, hopeless for 2×10⁵ of them. The fix is to take big upward jumps (powers of two) instead of single steps, which is exactly binary lifting.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
tree rooted at 0: 0-1,0-2,1-3,1-4,2-5; lca(3, 5)depths
depth[3]=2, depth[5]=2 — already equal
a=3, b=50C · Code with teaching comments
Reference implementation previewimport java.util.*; class NaiveLCA { private final List<List<Integer>> adj; private final int[] parent, depth;Explore the full codeHide the full code
D · Why it works and complexityO(n) per query after O(n) depth setup timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The two-phase structure — equalize depth, then climb together — is identical in the optimal solution. Binary lifting changes only how you climb: instead of one step at a time, you take the largest power-of-two jump that does not overshoot, cutting O(n) climbing to O(log n).
ComplexityO(n) per query after O(n) depth setup time, O(n) space — Equalizing depths is up to O(n) parent steps, and the lockstep walk is up to O(n) more. With q queries this is O(q·n) in the worst case (e.g. a chain).
E · Edge cases and pitfalls3Recursive DFS on a chain of 2×10⁵ nodes — stack overflow; use an explicit stack.Explore edge casesHide edge cases
Pitfalls
- Recursive DFS on a chain of 2×10⁵ nodes — stack overflow; use an explicit stack.
- Climbing before equalizing depths — the pointers can pass each other and never meet.
- Accepting O(n) per query — fine for one query, far too slow for many.
Approach 2 of 2 · Optimal
Binary Lifting — Sparse Ancestor Table (Optimal)
1Hint 1Explore hintHide hint
Store up[v][j] = the ancestor of v that is 2^j steps above it. up[v][0] is just the parent.
2Hint 2Explore hintHide hint
Fill the table by doubling: a 2^j jump is two 2^(j-1) jumps, so up[v][j] = up[ up[v][j-1] ][j-1]. Compute level by level.
3Hint 3Explore hintHide hint
Query in two phases: lift the deeper node by the depth gap (decomposed into powers of two), then for j from high to low, jump BOTH nodes up by 2^j whenever their ancestors there differ. The LCA is the parent of where they stop.
A · Core idea
Binary lifting precomputes jump pointers so any upward movement becomes a handful of power-of-two hops.
The table. Define up[v][j] as the ancestor reached by climbing 2^j parents from v. The base level is the parent: up[v][0] = parent[v]. The doubling recurrence builds the rest: climbing 2^j steps is the same as climbing 2^(j-1) steps and then 2^(j-1) steps again, so up[v][j] = up[ up[v][j-1] ][j-1]. Because each higher level depends only on the level below, fill the table column by column (all j-1 before any j). With LOG = ⌈log₂ n⌉ + 1 columns, the deepest possible jump is covered.
Jumping by an arbitrary amount. To climb exactly k steps, write k in binary and take one jump per set bit: for each bit j set in k, replace v with up[v][j]. Since k < n has at most log₂ n set bits, this is O(log n).
Query phase 1 — equalize depth. Suppose depth[a] ≥ depth[b]. Lift a up by depth[a] − depth[b] using the bit decomposition above, so both nodes sit at the same depth. If a == b now, one node was an ancestor of the other and that node is the LCA — return it immediately. Missing this check is a classic bug.
Query phase 2 — rise to just below the LCA. Now both nodes are at equal depth but on (possibly) different branches. For j from the highest level down to 0, look at up[a][j] and up[b][j]: if they are different, jump both up by 2^j (their ancestors still differ, so the LCA is higher). If they are the same, that jump would overshoot — skip it. After the loop, a and b sit on different branches but their parents coincide: the LCA is up[a][0].
Why scanning j from high to low works. It is a binary search on the climb distance. Greedily taking the biggest jump that keeps the two nodes distinct lands them exactly one step below the LCA — every set bit of the true distance gets taken once, none overshoots. The sentinel parent at the root makes 'jump past the root' return the root, so out-of-range jumps are harmless.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
tree rooted at 0: 0-1,0-2,1-3,1-4,2-5; lca(3, 5). depths: 3→2, 5→2.Interactive diagram
Two-phase query for lca(3,5)
build up[][0]Depths already equal; the high-to-low scan keeps both nodes on different branches, stopping at 1 and 2 whose shared parent 0 is the LCA.
0C · Code with teaching comments
D · Why it works and complexityO(n log n) preprocess, O(log n) per query timeO(n log n) spaceOpen the invariant and proofExplore the proofHide the proof
Binary lifting is the doubling trick applied to the parent function: precompute f^(2^j) so you can apply f any number of times in O(log n). The LCA query is then a depth alignment followed by a binary search for the highest divergence point. The exact same table answers k-th-ancestor and, via depths, path distance — one preprocessing, many queries.
ComplexityO(n log n) preprocess, O(log n) per query time, O(n log n) space — The table has n rows and log n columns, each filled in O(1) → O(n log n) build. A query does at most log n jumps to equalize depth plus log n jumps in the second phase → O(log n) per query. Space is the table, O(n log n).
E · Edge cases and pitfalls7lca(node, node) — Depths equal, a == b after phase 1 → returns the node itself.Explore edge casesHide edge cases
Edge cases handled
lca(node, node)Depths equal, a == b after phase 1 → returns the node itself.One node is an ancestor of the otherAfter equalizing depth the deeper node lifts onto the shallower one; the a == b check returns it as the LCA.Jump past the rootThe root's sentinel parent (itself) makes over-long jumps stay at the root, so up[][j] never indexes out of range.
Pitfalls
- Returning the meeting node from phase 2 instead of its parent (up[a][0]).
- Skipping the a == b check after depth alignment — wrong on the ancestor case.
- Too few columns (LOG) so a deep jump runs off the table.
- Recursive DFS for the depth/parent setup — stack overflow on a chain; use an explicit stack.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewimport java.util.*; class BinaryLiftingLCA { private final int LOG; private final int[][] up; // up[v][j] = 2^j-th ancestor of vExplore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Returning the stop node, not its parent
Phase 2 deliberately stops one level BELOW the LCA (where the two nodes are still on different branches). The answer is their common parent, up[a][0] — returning a itself gives a wrong, too-deep node.
Explore the code comparisonHide the code comparison
Returning the stop node, not its parent
Phase 2 deliberately stops one level BELOW the LCA (where the two nodes are still on different branches). The answer is their common parent, up[a][0] — returning a itself gives a wrong, too-deep node.
Don't write this
for j in range(self.LOG - 1, -1, -1):
if self.up[a][j] != self.up[b][j]:
a = self.up[a][j]; b = self.up[b][j]
return a # wrong: a is one level below the LCAWrite this instead
for j in range(self.LOG - 1, -1, -1):
if self.up[a][j] != self.up[b][j]:
a = self.up[a][j]; b = self.up[b][j]
return self.up[a][0] # the shared parent is the LCA2Missing the ancestor case
If b is an ancestor of a, then after lifting a to b's depth we get a == b. Without an early return, phase 2's loop never moves (ancestors are already equal at every level) and you return up[a][0] = the parent of b, which is wrong.
Explore the code comparisonHide the code comparison
Missing the ancestor case
If b is an ancestor of a, then after lifting a to b's depth we get a == b. Without an early return, phase 2's loop never moves (ancestors are already equal at every level) and you return up[a][0] = the parent of b, which is wrong.
Don't write this
int diff = depth[a] - depth[b];
for (int j = 0; j < LOG; j++) if (((diff>>j)&1)==1) a = up[a][j];
// missing: if (a == b) return a;
for (int j = LOG-1; j>=0; j--) { ... }
return up[a][0]; // returns parent of the true LCAWrite this instead
int diff = depth[a] - depth[b];
for (int j = 0; j < LOG; j++) if (((diff>>j)&1)==1) a = up[a][j];
if (a == b) return a; // one node was an ancestor of the other
for (int j = LOG-1; j>=0; j--) { ... }
return up[a][0];3Too few ancestor levels
If LOG is smaller than ⌈log2 n⌉ + 1, the largest jump cannot reach the root from the deepest node, so depth alignment and the rise loop both miss high ancestors.
Explore the code comparisonHide the code comparison
Too few ancestor levels
If LOG is smaller than ⌈log2 n⌉ + 1, the largest jump cannot reach the root from the deepest node, so depth alignment and the rise loop both miss high ancestors.
Don't write this
self.LOG = 16 # hard-coded; breaks when n needs more bitsWrite this instead
self.LOG = max(1, n.bit_length()) # scales with nRevise
Carry the pattern into the interview
Pattern to remember
Sparse ancestor table — jump in powers of two using up[v][j] = up[up[v][j-1]][j-1]
Code pattern to memorize
up[v][0] = parent[v]
up[v][j] = up[ up[v][j-1] ][j-1]
lca(a, b):
level a, b to equal depth, then
for j in high..0: if up[a][j] != up[b][j]: a, b = up[a][j], up[b][j]
return up[a][0]Rules to remember
- 1
up[v][0]is the direct parent; the root's parent is a sentinel (itself or 0/-1) so lifts past the root stay safe. - 2Allocate
LOG = ceil(log2(n)) + 1levels — enough to cover the deepest possible jump. - 3Equalize depths first: lift the deeper node by the depth difference, decomposed into powers of two.
- 4Then lift both nodes by the largest jump that keeps their ancestors different; the answer is the parent of where they stop.
- 5If after equalizing the nodes are already equal, that node is the LCA — handle it before the binary search.
Use this pattern when
- Many LCA queries on a static rooted tree (the tree does not change between queries)
- k-th ancestor queries, or distance(u, v) = depth[u] + depth[v] − 2·depth[lca]
- Jump-pointer problems: 'follow parent 2^j times' on any functional graph
- Path queries on trees that reduce to ancestor lookups
Choose another pattern when
- Tree changes between queries (links added/removed) → use link-cut trees or Euler-tour + segment tree
- Only one or two LCA queries → a single O(n) walk-up is simpler than O(n log n) preprocessing
- It is actually a BST → LCA is a single O(h) descent comparing values, no lifting needed
- You need LCA on a DAG, not a tree → binary lifting assumes a unique parent per node
Interview takeaway
Binary lifting turns 'walk up k steps' from O(k) into O(log k) by precomputing power-of-two jumps — the same doubling trick behind fast exponentiation and sparse tables. LCA falls out by aligning depths and then binary-searching the highest point where the two paths still diverge.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternBinary lifting: precompute up[v][j] = the 2^j-th ancestor via the doubling recurrence up[v][j] = up[up[v][j-1]][j-1]. Answer LCA by equalizing depth then binary-searching the highest divergence point — O(n log n) build, O(log n) query. The same table powers k-th ancestor and path-distance queries on static rooted trees.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Lowest Common Ancestor of a Binary TreeFor a single query on a general binary tree, a one-pass recursion is simpler than building lifting tables.
- Kth Ancestor of a Tree NodeComing soonExactly the up[][] table — decompose k into set bits and take one jump per bit.
- Distance Between Two NodesComing soondepth[u] + depth[v] − 2·depth[lca(u, v)] reuses the same preprocessing.
- Euler Tour of a TreeAn alternative LCA route: Euler tour + range-minimum on depths answers LCA in O(1) after O(n log n) build.