LCA Postorder Review

What We Already Know
Back in Chapter 3, you learned to find the Lowest Common Ancestor of two nodes by walking up the tree. The idea is simple: bring both nodes to the same depth, then walk both up in lockstep until they meet.
This works. It's correct. And for a single query, it's perfectly fine.
The trouble starts when you have \(10^5\) queries.
The Naive Algorithm
Given a rooted tree where each node knows its parent and depth:
- If \(depth[u] > depth[v]\), walk \(u\) up until it reaches the same depth as \(v\).
- If \(depth[v] > depth[u]\), walk \(v\) up until it reaches the same depth as \(u\).
- Now both are at the same depth. Walk both up one step at a time until they meet.
The meeting point is the LCA.
Trace on this tree:
Depths: \(depth[1]=0, depth[2]=1, depth[3]=1, depth[4]=1, depth[5]=2, depth[6]=2, depth[7]=2, depth[8]=3\).
Query: LCA(8, 6)?
| Step | \(u\) | \(v\) | Action |
|---|---|---|---|
| 0 | 8 (depth 3) | 6 (depth 2) | \(u\) is deeper, walk up |
| 1 | 5 (depth 2) | 6 (depth 2) | same depth, walk both |
| 2 | 2 (depth 1) | 2 (depth 1) | \(u = v\), done |
\(LCA(8, 6) = 2\). Three steps.
Query: LCA(8, 7)?
| Step | \(u\) | \(v\) | Action |
|---|---|---|---|
| 0 | 8 (depth 3) | 7 (depth 2) | \(u\) is deeper, walk up |
| 1 | 5 (depth 2) | 7 (depth 2) | same depth, walk both |
| 2 | 2 (depth 1) | 4 (depth 1) | different, walk both |
| 3 | 1 (depth 0) | 1 (depth 0) | \(u = v\), done |
\(LCA(8, 7) = 1\). Four steps.
Query: LCA(3, 3)?
Already the same node. \(LCA = 3\). Zero steps.
The Cost
Each query takes \(O(depth)\) steps in the worst case. On a balanced tree with \(n\) nodes, depth is \(O(\log n)\), so each query is \(O(\log n)\). Not bad.
But trees aren't always balanced. A chain (1 → 2 → 3 → ... → \(n\)) has depth \(n - 1\). A single LCA query could take \(O(n)\) steps.
For \(q\) queries on a tree with \(n\) nodes:
- Worst case: \(O(nq)\). With \(n = q = 2 \times 10^5\), that's \(4 \times 10^{10}\). Far too slow.
- Best case (balanced tree): \(O(q \log n)\). Fast enough — but you can't count on the tree being balanced.
The Code
vector<int> adjacency[200005];
int parentOf[200005];
int depthOf[200005];
void dfs(int node, int par, int currentDepth) {
parentOf[node] = par;
depthOf[node] = currentDepth;
for (int child : adjacency[node]) {
if (child != par) {
dfs(child, node, currentDepth + 1);
}
}
}
int lcaNaive(int u, int v) {
while (depthOf[u] > depthOf[v]) u = parentOf[u];
while (depthOf[v] > depthOf[u]) v = parentOf[v];
while (u != v) {
u = parentOf[u];
v = parentOf[v];
}
return u;
}
Complexity: \(O(n)\) preprocessing, \(O(n)\) per query worst case.
What We Need
The goal for this chapter: answer \(q\) LCA queries on a tree with \(n\) nodes, where both \(n\) and \(q\) can be up to \(2 \times 10^5\).
| Method | Preprocessing | Per query | Total |
|---|---|---|---|
| Naive (this lesson) | \(O(n)\) | \(O(n)\) | \(O(nq)\) |
| Binary lifting (Lesson 2) | \(O(n \log n)\) | \(O(\log n)\) | \(O(n \log n + q \log n)\) |
| Euler tour + RMQ (Lesson 3) | \(O(n \log n)\) | \(O(1)\) | \(O(n \log n + q)\) |
Both advanced methods spend \(O(n \log n)\) upfront to precompute a table, then answer each query fast. The tradeoff: binary lifting is simpler to code and also gives you \(k\)-th ancestor queries for free. Euler tour + RMQ is faster per query but does nothing beyond LCA.
The naive method isn't useless — it's the right choice when you have exactly one LCA query, or when \(n\) is small enough that \(O(n)\) per query doesn't matter. But for competitive programming constraints, you need one of the next two methods.
Try It
Fill in the lcaNaive function in the starter code.
Predict before running: On the chain 1 → 2 → 3 → 4 → 5, what is \(LCA(5, 3)\)? Node 5 is at depth 4, node 3 is at depth 2. Walk node 5 up twice: 5 → 4 → 3. Now \(u = v = 3\). Answer: 3.
Challenge: What if you're given \(10^5\) queries but the tree is guaranteed to be a complete binary tree with \(n = 2^{17} - 1 \approx 1.3 \times 10^5\) nodes? The depth is at most 16. The naive approach takes at most 32 steps per query. Total: \(3.2 \times 10^6\) — perfectly fine. Know your constraints before over-engineering.
Edge Cases to Watch For
- One node is a direct ancestor of the other: After equalizing depths, \(u = v\) immediately. If your code skips the "walk up together" phase when \(u = v\) after depth equalization, this works. But if it doesn't check \(u = v\) before the lockstep phase, it walks past the answer.
Problems
| Problem | Link | Difficulty |
|---|---|---|
| LC 236 — LCA of a Binary Tree | leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree | Medium |
| CSES — Company Queries II | cses.fi/problemset/task/1688 | Medium |