PATTERN 2 — TREE RECURSION#
2.1 Pattern Breakdown#
Pattern 1 asks "in what order do I touch the nodes?" Pattern 2 asks a harder question: "what does a subtree owe its parent?"
Every problem here is solved by answering three questions, in this order, before writing code:
- What does
dfs(node)return? One number? A pair? A structure? Name the type. - What is the value for
null? It must be the identity element of the combining operation —0for a sum,0for a height,truefor an "all nodes satisfy",Integer.MIN_VALUEfor an unclamped max. - Is the answer the return value, or something recorded on the side? If those differ, you are in sub-variant C and the two must never be confused.
| # | Sub-variant | What dfs returns | Direction | Answer lives in |
|---|---|---|---|---|
| A | Bottom-up aggregate | one value about the subtree | up | the return value |
| B | Parallel recursion on two trees | a boolean or a merged node | up | the return value |
| C | Augmented return | the value the parent needs | up | a field/array on the side |
| D | Top-down inherited state | nothing (or a count) | down | an accumulator, via parameters |
| E | Root-to-leaf paths + backtracking | nothing | down | a mutable path, undone on exit |
| F | Prefix sums on the root path | nothing | down | a map keyed by prefix, undone on exit |
| G | LCA | the node found, or null | up | the return value |
| H | Construction from traversals | the built subtree | up | the return value |
| I | Serialization / structural identity | a canonical string or hash | up | a map or a string |
| J | In-place restructuring | the new subtree root | up | the tree itself |
| K | Tree DP with per-child states | a small tuple, one entry per state | up | a combination of the root's states |
| L | Rerooting (two-pass) | pass 1 up, pass 2 down | both | an array indexed by node |
Sub-variants worth stating explicitly:
- C is where interviews are lost. In 543 and 124 the value you record (a path through the node) and the value you return (a path ending at the node, usable by the parent) are different quantities. People who return the recorded value get plausible-looking wrong answers.
- E and F look like DFS but are really backtracking: every mutation on the way down needs an exact inverse on the way up.
- K is dynamic programming that happens to run on a tree. The state is per node, the transition is over children, and the order is postorder because that is the only topological order available.
- L is the only sub-variant where a single DFS is provably insufficient: you need every node's answer, and each answer depends on the whole rest of the tree.
2.2 Problem Table#
A Bottom-up aggregate#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 27★ | 104. Maximum Depth of Binary Tree | Easy | A | The atom of postorder aggregation: 1 + max(left, right), identity 0. Every Pattern 2 problem is a mutation of this shape. | |
| 28★ | 110. Balanced Binary Tree | Easy | A | The sentinel-abort idiom: return -1 to mean "already unbalanced" and the whole tree is O(n) instead of O(n log n). This is sub-variant C compressed into one integer. | |
| 29⚠︎ | 222. Count Complete Tree Nodes | Medium | A | A correct O(n) traversal is the wrong answer. The constraint "complete" is the problem: compare left- and right-spine heights and discard half the tree per level → O(log²n). Whenever a tree problem names its shape, the shape is the algorithm. | |
| 30○ | 404. Sum of Left Leaves | Easy | A | Aggregate with a predicate that depends on the edge you arrived by, not the node. Two-minute rep. |
B Parallel recursion on two trees#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 31★ | 100. Same Tree | Easy | B | Two cursors descending in lockstep. The three-line base case (both null / one null / values differ) is the template for everything in B. | |
| 32★ | 101. Symmetric Tree | Easy | B | Same machine with the recursion crossed: (a.left, b.right) and (a.right, b.left). Teaches that the pairing rule is a parameter, not a law. | |
| 33★ | 572. Subtree of Another Tree | Easy | B | Composition: a traversal whose visit action is a whole second recursion. O(mn) is expected; know that the O(m+n) answer is serialization plus KMP (see #55). | |
| 34○ | 617. Merge Two Binary Trees | Easy | B | B where the return is a node instead of a boolean. Redundant if #31 is solid. | |
| 35○ | 951. Flip Equivalent Binary Trees | Medium | B | B with a disjunction over two pairings. Good if you want one harder rep. |
C Augmented return: what you record ≠ what you return#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 36⚠︎ | 543. Diameter of Binary Tree | Easy | C | The proof problem of Pattern 2. The obvious recursion returns the diameter and is wrong: a parent cannot build a path from a child's diameter. Return the height; record left + right in a field. Be able to say that out loud. | |
| 37★ | 124. Binary Tree Maximum Path Sum | Hard | C | #36 with two additions: the returned value is clamped at zero (Math.max(0, child) — a negative branch is simply not used) and the recorded value may be a single node. The clamp is the entire difficulty. | |
| 38○ | 687. Longest Univalue Path | Medium | C | Same skeleton with an equality guard on each edge. Pure rep of the record/return split. | |
| 39○ | 1372. Longest ZigZag Path in a Binary Tree | Medium | C | The returned value becomes a pair (left-going, right-going). Do this if you want to see the pattern generalize beyond one number. |
D Top-down inherited state#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 40★ | 1448. Count Good Nodes in Binary Tree | Medium | D | The cleanest statement of "the parameter is the state." maxSoFar flows down; nothing flows up but a count. | |
| 41★ | 129. Sum Root to Leaf Numbers | Medium | D | The accumulator is transformed on the way down (cur * 10 + val) and only harvested at leaves. Watch what "leaf" means — not "null". | |
| 42○ | 1315. Sum of Nodes with Even-Valued Grandparent | Medium | D | Two generations of inherited state instead of one. Skip if #40 was clean. |
E Root-to-leaf paths with backtracking#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 43★ | 112. Path Sum | Easy | E | The leaf test, isolated. root == null returning false is correct; root.left == null && root.right == null is the actual leaf condition. Most people conflate them once, forever. | |
| 44★ | 257. Binary Tree Paths | Easy | E | First problem where the path is a mutable list: add before recursing, remove after. The remove is the whole sub-variant. | |
| 45★ | 113. Path Sum II | Medium | E | #43 and #44 composed, plus the copy-on-record rule: new ArrayList<>(path), or every result aliases the same list and you return N copies of the empty list. | |
| 46○ | 988. Smallest String Starting From Leaf | Medium | E | Backtracking with a StringBuilder and a reversal. Good if setLength discipline is shaky. |
F Prefix sums on the root path#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 47★ | 437. Path Sum III | Medium | F | The bridge from arrays to trees: the prefix-sum hash map from "subarray sum equals K" transplanted onto the root path. The map must be undone on the way up or paths from different branches contaminate each other. If you know LC 560, you already know this — that transfer is the point. |
G Lowest common ancestor#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 48★ | 236. Lowest Common Ancestor of a Binary Tree | Medium | G | Six lines that hide a real proof: returning a non-null from both sides means this node is the split point. Be able to explain why returning the node itself on a match is correct even when one target is an ancestor of the other. | |
| 49★ | 1123. Lowest Common Ancestor of Deepest Leaves | Medium | G + C | LCA where the targets are not given — you must return (depth, node) upward and let the deeper side win. Sub-variants C and G composed. | |
| 50○ | 1650. Lowest Common Ancestor of a Binary Tree III PRO | Medium | G | With parent pointers this becomes the two-pointer "intersection of two linked lists" trick. Free substitute: 160. Intersection of Two Linked Lists. |
H Construction from traversals#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 51★ | 105. Construct Binary Tree from Preorder and Inorder Traversal | Medium | H | Preorder gives you the root; inorder tells you how much of it belongs to the left. The index map plus the arithmetic for the two sub-ranges is the entire problem, and it is always an off-by-one. | |
| 52★ | 106. Construct Binary Tree from Inorder and Postorder Traversal | Medium | H | Postorder consumed from the right, and therefore the right subtree is built before the left. Write it immediately after #51 or you will fuse the two in memory. | |
| 53○ | 889. Construct Binary Tree from Preorder and Postorder Traversal | Medium | H | Shows why the answer is not unique without inorder. Worth reading even if you don't code it. | |
| 54○ | 654. Maximum Binary Tree | Medium | H | Construction from one array by repeatedly splitting at the max. O(n) monotonic-stack solution is a bonus. |
I Serialization and structural identity#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 55★ | 297. Serialize and Deserialize Binary Tree | Hard | I | The null markers are the structure. Preorder with # for null is uniquely decodable; preorder without it is not, and inorder is not even with markers. Deserialization consumes the same stream in the same order — one shared cursor, never an index copy. | |
| 56○ | 652. Find Duplicate Subtrees | Medium | I | Canonical serialization used as a map key. The obvious string concatenation is O(n²) characters; know the id-triple alternative. | |
| 57○ | 449. Serialize and Deserialize BST | Medium | I + P3 | The BST version needs no null markers at all — the ordering carries the structure. Nice contrast with #55. |
J In-place restructuring#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 58★ | 226. Invert Binary Tree | Easy | J | The whole of J in three lines: recurse, then rewire. Swap after the recursive calls return, or you invert the subtrees you already swapped. | |
| 59★ | 114. Flatten Binary Tree to Linked List | Medium | J | Two solutions worth knowing: reverse-postorder with a prev pointer, and the O(1)-space Morris-style rewiring. The second is the follow-up they actually want. | |
| 60★ | 117. Populating Next Right Pointers in Each Node II | Medium | J + D | The O(1)-space level-order: the level you already linked is the queue for the next one. A dummy head plus a tail pointer removes every special case. | |
| 61○ | 116. Populating Next Right Pointers in Each Node | Medium | J | The perfect-tree version. Strictly easier than #60 — do it only as a warm-up. | |
| 62○ | 156. Binary Tree Upside Down PRO | Medium | J | Rewiring along the left spine. Free substitute: 114. |
K Tree DP with per-child states#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 63★ | 337. House Robber III | Medium | K | The int[]{withRoot, withoutRoot} return. The first genuinely two-dimensional state, and the model for everything else in K. | |
| 64★ | 979. Distribute Coins in Binary Tree | Medium | K | The return value is a surplus that may be negative, and the cost is abs(left) + abs(right). Teaches that flow across an edge is the thing being counted, not nodes. | |
| 65★ | 968. Binary Tree Cameras | Hard | K | Three states (covered-with-camera / covered-without / uncovered) and a greedy postorder placement. The hardest state design in this document; if you can derive its transition table you can derive any of them. | |
| 66○ | 1339. Maximum Product of Splitted Binary Tree | Medium | K | Subtree sums plus a second pass over candidates. Watch the modulus: take it only at the very end, on the maximum. |
L Rerooting and two-pass DFS#
| Solved | # | Problem | Diff | Sub-variant | Why it's essential |
|---|---|---|---|---|---|
| 67★ | 834. Sum of Distances in Tree | Hard | L | The one problem that proves a single DFS is not enough. Pass 1 computes subtree sizes and the root's answer bottom-up; pass 2 derives every child's answer from its parent's in O(1): ans[c] = ans[p] + (n - 2 * size[c]). Derive that formula, don't memorize it. | |
| 68○ | 310. Minimum Height Trees | Medium | L | Topological leaf-peeling — a different, cheaper way to exploit "every node needs an answer". Good contrast. |
Extra Reps — Tree Recursion (only if a gate fails)#
| Solved | Problem | Targets |
|---|---|---|
| 508. Most Frequent Subtree Sum | Bottom-up aggregate feeding a frequency map. | |
| 1026. Maximum Difference Between Node and Ancestor | Top-down min/max state, sub-variant D. | |
| 863. All Nodes Distance K in Binary Tree | Re-rep of the tree-as-graph transfer if §1.F failed. | |
| 1443. Minimum Time to Collect All Apples in a Tree | Tree DP on an adjacency list rather than a TreeNode. | |
| 2246. Longest Path With Different Adjacent Characters | Sub-variant C on a general tree — the diameter argument again. | |
| 545. Boundary of Binary Tree PRO | Three separate traversals composed. Free substitute: 199 plus 257. |
2.3 Templates#
A Bottom-up aggregate#
Ask each subtree for one fact about itself, then combine the two answers. Nothing above the node is visible, and nothing above it needs to be.
Mental model
“dfs(n) answers a question about the subtree at n, using only the two answers its children gave me. I never look up, and I never need to.”
Pattern 1 asked in what order do I touch the nodes? This pattern asks a harder question: what does a subtree owe its parent? Answer it before typing and most of the bugs never get written.
Three questions, in this order, every single time: what does dfs return; what is the value for null; is the answer the return value or something recorded on the side.
Get the identity wrong and every leaf reports a wrong answer, which then poisons everything above it. It is the single most common source of off-by-one in this pattern.
Recognition — reach for this when
- The answer is one fact per subtree — a height, a count, a sum, a boolean.
- A node's answer is computable from its two children's answers and nothing else.
- You can name the value for
nullwithout hesitating. - But not when what you record differs from what the parent needs. That is sub-variant C, and confusing the two is where interviews are lost.
JavaINVARIANT: dfs(n) returns a fact about the subtree rooted at n, computed only from facts8 lines
// INVARIANT: dfs(n) returns a fact about the subtree rooted at n, computed only from facts
// about its two children. Nothing above n is visible, and nothing needs to be.
// IDENTITY: the null case must be the neutral element of the combining operation —
// 0 for a height or a sum, true for a universal claim, MIN_VALUE for an open max.
int height(TreeNode n) {
if (n == null) return 0;
return 1 + Math.max(height(n.left), height(n.right));
}Java110. The sentinel-abort idiom. -1 means "a subtree below is already unbalanced", which8 lines
// 110. The sentinel-abort idiom. -1 means "a subtree below is already unbalanced", which
// short-circuits everything above it: O(n) instead of the naive O(n log n).
int balancedHeight(TreeNode n) {
if (n == null) return 0;
int l = balancedHeight(n.left); if (l < 0) return -1;
int r = balancedHeight(n.right); if (r < 0) return -1;
return Math.abs(l - r) > 1 ? -1 : 1 + Math.max(l, r);
}Java222. "Complete" is the algorithm, not decoration. Equal spine heights ⇒ the subtree is15 lines
// 222. "Complete" is the algorithm, not decoration. Equal spine heights ⇒ the subtree is
// perfect ⇒ 2^h - 1 nodes with no traversal at all. Otherwise exactly one of the two
// recursive calls goes deep, so the total is O(log^2 n).
int countNodes(TreeNode root) {
if (root == null) return 0;
int lh = spine(root, true), rh = spine(root, false);
if (lh == rh) return (1 << lh) - 1;
return 1 + countNodes(root.left) + countNodes(root.right);
}
int spine(TreeNode n, boolean left) {
int h = 0;
while (n != null) { h++; n = left ? n.left : n.right; }
return h;
}Why it works — the three questions, and why postorder is forced4 steps
This sub-variant is the base case of the whole pattern, so it is worth being explicit about why the shape is what it is.
- 1
What does
dfsreturn? Name the type before writing anything. One number? A boolean? A pair? Ambiguity here is what produces functions that half-return two different quantities. - 2
What is the value for
null? It must be the identity element of the combining operation —0for a sum or a height,truefor a universal claim,Integer.MIN_VALUEfor an unclamped max. Ask what the empty subtree contributes. - 3
Is the answer returned or recorded? For sub-variant A they are the same value, which is exactly what makes it the easy case. When they diverge you are in C.
- 4
Why the visit must be postorder. Both children's answers must already exist when the parent combines them, and postorder is the only order where both recursive calls have returned — the point established in Pattern 1 A.
Answer these three before typing: what does dfs return; what is the value for null; is the answer the return value or a recorded one. Every bug in this pattern is one of those three answered wrongly.
LC 110's sentinel is a real complexity fix, not a trick. Returning -1 for something below me is already unbalanced short-circuits every ancestor, giving O(n). Computing height inside an outer traversal instead is O(n log n) on balanced trees and O(n^2) on skewed ones.
LC 222 turns a property into an algorithm. Complete means equal left and right spine heights imply a perfect subtree, so its size is 2^h - 1 with no traversal at all. When they differ, exactly one of the two recursive calls descends — hence O(log^2 n).
Use long for sums. LC 129, 1339 and 437 all overflow int on deep or wide trees, silently. Take any modulus at the end, not during.
Walkthrough — LC 110 — the sentinel doing its job4 steps
dfs returns the height, or -1 meaning already unbalanced. Watch how the -1 at node 2 stops all work above it.
| # | Node | left | right | Combine | Returns |
|---|---|---|---|---|---|
| 1 | 4 | 0 (null) | 0 (null) | |0-0| <= 1, ok | 1 |
| 2 | 3 | 1 | 0 (null) | |1-0| <= 1, ok | 2 |
| 3 | 2 | 2 | 0 (null) | |2-0| = 2 > 1 -> unbalanced | -1 |
| 4 | 1 | -1 | -- | left is already -1 | -1, short-circuit — node 9 is never visited |
Answer false. Row 4 is the point: once -1 appears, no ancestor does any further work and the right subtree is never explored at all. The naive version — a height() call inside an outer isBalanced() traversal — would recompute heights at every level instead, which is where the extra log n factor comes from.
Key observations — what interviewers are listening for4 points
- Say the three questions out loud, in order. Return type, null value, returned-or-recorded. The gate for this sub-variant is literally answering them for LC 104.
- The identity element is derived, not recalled. What does the empty subtree contribute to this combine? Sum: 0. Height: 0. Universal claim: true. Max with no floor:
MIN_VALUE. - A sentinel value is a legitimate return type.
-1in LC 110 is not a hack — it is a second channel in the return, and it buys a whole complexity class. Explain theO(n)versusO(n log n)difference unprompted. - Watch for problems where the constraint is the algorithm. LC 222's complete is the entire solution. Reading a constraint as decoration rather than as a lever is a recurring way to miss the intended approach.
Common mistakes4 traps
-
Wrong identity for
nullSymptom: off-by-one at every leaf, propagating up the whole tree.
Prevention: Ask what the empty subtree contributes to this specific combine, and answer per state.
-
Recomputing height inside an outer traversal (LC 110)
Symptom:
O(n log n)on balanced trees,O(n^2)on skewed ones.Prevention: Use the
-1sentinel and a single pass. -
intsums on LC 129, 1339 or 437Symptom: silent overflow on deep or wide trees.
Prevention: Accumulate in
long; apply any modulus only at the end. -
Combining before both calls have returned
Symptom: you are writing preorder and calling it an aggregate.
Prevention: Aggregates are postorder. Both children must be known before the parent combines.
Key takeaway
- Trigger: one fact per subtree, computable from the two children's facts.
- The three questions: what is returned, what
nullreturns, returned-or-recorded. - Identity: the neutral element of the combine — 0,
true,MIN_VALUEas appropriate. - LC 110:
-1sentinel givesO(n); the naive nesting givesO(n log n). - Gate: answer the three questions aloud for LC 104, and write LC 110's sentinel version with the complexity argument. See §5.2.
B Parallel recursion on two trees#
Walk two trees in lockstep with one function. The base case does all the work; the recursive step is a single line.
Mental model
“I am comparing two nodes at the same position in two trees. Either both are absent, or one is, or both exist — and only the third case needs any thought.”
The three-case base is the whole template, and a == b is doing something slightly clever: it covers both null in one line, and it is the only place two nulls are allowed to compare equal.
The pairing rule — which child of a lines up with which child of b — is a parameter, not a law. Changing it is what turns same tree into symmetric tree.
a == b is true only when both are the same reference — which for two independent trees means both are null. That is why it collapses the both-null case into one comparison.
Recognition — reach for this when
- Two trees, compared or merged position by position.
- The question is structural — identical, symmetric, subtree-of, merged.
- You can state the pairing rule in one line.
- But not when the two trees are traversed at different rates. Matching a subtree at any position needs an outer loop over candidate roots as well.
JavaThe three-case base is the whole template. "a == b" covers both-null in one line and is12 lines
// The three-case base is the whole template. "a == b" covers both-null in one line and is
// the only place two nulls are allowed to be equal.
boolean same(TreeNode a, TreeNode b) {
if (a == null || b == null) return a == b;
return a.val == b.val && same(a.left, b.left) && same(a.right, b.right);
}
// 101. Identical machine, CROSSED pairing. The pairing rule is a parameter, not a law.
boolean mirror(TreeNode a, TreeNode b) {
if (a == null || b == null) return a == b;
return a.val == b.val && mirror(a.left, b.right) && mirror(a.right, b.left);
}Why it works — the three-case base, and why symmetry is a parameter4 steps
Almost all the correctness lives in the base case, which is why it is worth writing carefully once and reusing.
- 1
Both absent.
a == bcatches it. Two nulls are the same reference, so this istrue— structurally identical empty subtrees. - 2
Exactly one absent. If the previous test failed and either is
null, the structures differ.false, immediately. - 3
Both present. Compare the values. If they differ the trees differ; if they match, the answer depends entirely on the children.
- 4
The pairing rule. For same tree you pair left-with-left. For symmetric you pair left-with-right. Same function, same base case, one argument swapped — which is why LC 101 is not a new algorithm.
The distinction the gate asks for: the crossed pairing in LC 101 is a parameter, not a different algorithm. One function walks two trees; how you line up the children is the only thing that changes.
Merging returns a node instead of a boolean. LC 617 uses the same skeleton with the base cases returning the surviving subtree rather than true/false — the shape is identical, only the return type moves.
Subtree-of is this plus an outer search. LC 572 runs the two-tree comparison from every candidate root, which is why its cost is the product rather than the sum.
Walkthrough — LC 101 — symmetric check with the crossed pairing4 steps
Watch the pairing column. Each call compares a node from the left subtree against its mirror in the right subtree.
| # | Compared pair | Case | Result |
|---|---|---|---|
| 1 | (2, 2) | both present, values equal | recurse on the crossed pairs |
| 2 | (3, 3) | outer-left vs outer-right, equal | recurse -> both children null |
| 3 | (4, 4) | inner-left vs inner-right, equal | recurse -> both children null |
| 4 | (null, null) | a == b | true — the base case that terminates every branch |
Answer true. The only thing separating this from LC 100 is which children were paired at step 1: (a.left, b.right) and (a.right, b.left) instead of the straight pairing. Same base case, same recursion, one argument order.
Key observations — what interviewers are listening for4 points
- Write the base case once and reuse it. Three lines, and they cover every structural comparison in this sub-variant.
a == bis deliberate, not lazy. It handles both-null in a single reference comparison. Worth pointing out, because it looks like a shortcut and is actually the precise test.- Name the pairing before you write the recursion. Left with right or left with left. Stating it turns LC 101 from a puzzle into a one-argument change.
- The return type can be a node. Merging is the same walk with a different payload. Recognising that keeps LC 617 from feeling like a new problem.
Common mistakes4 traps
-
Checking
a == null && b == nullthena == null || b == nullseparatelySymptom: correct but verbose, and easy to get the order wrong.
Prevention:
a == bcollapses the first case cleanly. Then a single||test. -
Using the straight pairing for LC 101
Symptom: you have written same tree and answered a different question.
Prevention: Symmetric pairs left-with-right. State the pairing explicitly before recursing.
-
Comparing values before checking for null
Symptom:
NullPointerExceptionon the first lopsided pair.Prevention: Base cases first, always: both-null, one-null, then values.
-
Assuming subtree-of is a single comparison
Symptom: misses matches that start below the root.
Prevention: LC 572 needs an outer walk over candidate roots as well.
Key takeaway
- Trigger: two trees compared or merged position by position.
- The base:
a == b-> true; one null -> false; values differ -> false. - The pairing: a parameter. Straight for LC 100, crossed for LC 101.
- Return type: boolean for comparison, node for merging — same skeleton.
- Gate: the two-tree base case blind, plus why LC 101's crossed pairing is a parameter. See §5.2.
C Augmented return: record ≠ return#
The value you record and the value you return are two different quantities. Confusing them produces answers that look right and are not.
Mental model
“At this node I can measure the best path that bends here — but a bend is useless to my parent, because it has already spent both of my sides. What my parent can use is a path that still continues upward. So I record one and return the other.”
This is where interviews are lost. In LC 543 and 124 the recorded value (a path through the node) and the returned value (a path ending at the node, usable by the parent) are genuinely different quantities, and returning the recorded one gives plausible-looking wrong answers.
Say it out loud before coding: record the bend, return the continuation.
A bent path has already consumed both of the node's sides, so no ancestor can extend it. That is the whole reason the two values must differ.
Recognition — reach for this when
- The best answer might sit entirely below the root, bending at some inner node.
- You catch yourself wanting to return two different things from one function.
- Words like diameter, maximum path, longest path that need not touch the root.
- But not when the recorded and returned values coincide. Then it is sub-variant A and the extra machinery is noise.
Java543. Diameter. THE distinction of Pattern 2:13 lines
// 543. Diameter. THE distinction of Pattern 2:
// RECORD left + right — the path that BENDS at this node; unusable by the parent
// RETURN 1 + max(left,right)— the path that CONTINUES upward; the only thing a parent wants
// Returning the diameter is the classic wrong answer: a parent cannot build a longer path
// out of a child's bent path, because a bent path already used both of the child's sides.
int best = 0;
int heightForDiameter(TreeNode n) {
if (n == null) return 0;
int l = heightForDiameter(n.left), r = heightForDiameter(n.right);
best = Math.max(best, l + r);
return 1 + Math.max(l, r);
}Java124. Maximum path sum = 543 plus one clamp.12 lines
// 124. Maximum path sum = 543 plus one clamp.
// Math.max(0, child) encodes "a branch with a negative total is simply not taken".
// Without it, a tree of all-negative values returns a sum of several of them.
int bestSum = Integer.MIN_VALUE;
int gain(TreeNode n) {
if (n == null) return 0;
int l = Math.max(0, gain(n.left));
int r = Math.max(0, gain(n.right));
bestSum = Math.max(bestSum, n.val + l + r); // RECORD: the path may bend here
return n.val + Math.max(l, r); // RETURN: a path going up cannot bend
}Why it works — why a bent path cannot be returned4 steps
One argument, and once it lands the whole sub-variant is obvious.
- 1
Two different questions at each node. What is the best path that passes through this node? and what is the best path ending at this node that my parent could extend? They have different answers.
- 2
The bend uses both sides.
left + rightgoes down one child, through the node, and down the other. Every one of the node's connections is now spent. - 3
So a parent cannot extend it. To reach the node from above, the parent needs the node's upward connection — which a bent path has already used. There is no way to attach.
- 4
Hence record one, return the other. Record
left + rightinto a running best; return1 + max(left, right), a path that has one side free and can therefore be continued.
Say it out loud before coding: record the bend, return the continuation. Returning the recorded value is the classic wrong answer — a parent cannot build a longer path out of a child's bent path, because a bent path already used both of the child's sides.
LC 124 is LC 543 plus one clamp. Math.max(0, child) encodes a branch with a negative total is simply not taken. Without it, a tree of all-negative values returns a sum of several of them instead of the single least-bad node.
The clamp belongs on the child's contribution, not on the result. Clamping the final answer would forbid a legitimately negative maximum on an all-negative tree; clamping the child is what expresses I may decline this branch.
Walkthrough — LC 543 — where record and return diverge5 steps
The best path here bends at node 2, well below the root. Watch the two columns come apart.
| # | Node | left ht | right ht | RECORD (bend) | RETURN (continue) |
|---|---|---|---|---|---|
| 1 | 4 | 0 | 0 | 0 + 0 = 0 | 1 + max(0,0) = 1 |
| 2 | 5 | 0 | 0 | 0 | 1 |
| 3 | 2 | 1 | 1 | 1 + 1 = 2 | 1 + max(1,1) = 2 |
| 4 | 3 | 0 | 0 | 0 | 1 |
| 5 | 1 | 2 | 1 | 2 + 1 = 3 <- the answer | 1 + max(2,1) = 3 |
The diameter is 3 edges — the path 4 - 2 - 1 - 3, recorded at the root in row 5. Row 3 is where the two columns first diverge: node 2 records a bend of 2 — the path 4 - 2 - 5, already finished — while returning 2 as a continuation, the path 4 - 2, which still has a free end pointing up. The root can extend the second and could never extend the first.
Key observations — what interviewers are listening for4 points
- This is the sub-variant that decides the pattern. The gate asks you to state the record/return distinction for LC 543 unprompted. Producing the code without the sentence is exactly what it is designed to catch.
- Name both quantities before writing the function. The bend and the continuation. Two names, two lines, and the bug becomes unwriteable.
- The clamp is a modelling decision.
max(0, child)says I am allowed to decline this branch. Justify it on an all-negative tree and it stops being a magic zero. - A running best is legitimate mutable state. Recording into a field while returning something else is not a hack — it is what makes one postorder pass sufficient.
Common mistakes4 traps
-
Returning the recorded value
Symptom: plausible but wrong on any tree where the best path bends below the root.
Prevention: Say it before coding: record the bend, return the continuation.
-
Omitting
Math.max(0, child)in LC 124Symptom: wrong on all-negative trees — it sums several negatives instead of taking the best single node.
Prevention: A branch with a negative total is never taken. The clamp encodes that.
-
Clamping the recorded answer instead of the child
Symptom: an all-negative tree wrongly reports 0.
Prevention: Clamp the child's contribution. The answer itself may legitimately be negative.
-
Using one variable for both quantities
Symptom: the two meanings collide and the bug is invisible on symmetric test trees.
Prevention: Two names. The compiler cannot catch this one for you.
Key takeaway
- Trigger: the best answer may bend at an inner node and never touch the root.
- The rule: record the bend, return the continuation.
- LC 543: record
left + right; return1 + max(left, right). - LC 124: the same, plus
max(0, child)— decline branches with negative totals. - Gate: state the distinction for LC 543 unprompted, then write LC 124 with the clamp justified on an all-negative tree. See §5.2.
D Top-down inherited state#
Push what you know down as a parameter. Nothing comes back up except a count — and because each child recomputes the state from scratch, there is nothing to undo.
Mental model
“The parameter is the state. I hand each child the version of it that applies to them, and I never have to restore anything, because I never modified anything shared.”
This is the mirror image of sub-variant A: information flows down rather than up. The answer accumulates in a counter or is returned as a simple sum of the children's counts.
The reason D needs no backtracking — unlike E and F — is that the state is passed by value and recomputed per child, rather than mutated in a shared structure.
max(maxSoFar, node.val) creates a fresh value for the call rather than editing something both siblings can see. That is what makes backtracking unnecessary.
Recognition — reach for this when
- A node's verdict depends on the path from the root to it — a running max, depth, or sum.
- The answer is a count or a simple sum over all nodes.
- The inherited state is a small immutable value you can pass as a parameter.
- But not when the state is a shared mutable structure. A list or a map that both children see needs the undo discipline of E and F.
Java1448. The parameter IS the state. Nothing comes back up except a count, and the state is9 lines
// 1448. The parameter IS the state. Nothing comes back up except a count, and the state is
// recomputed for each child rather than restored — which is why D needs no backtracking.
int goodNodes(TreeNode n, int maxSoFar) {
if (n == null) return 0;
int self = n.val >= maxSoFar ? 1 : 0;
int m = Math.max(maxSoFar, n.val);
return self + goodNodes(n.left, m) + goodNodes(n.right, m);
}
// call: goodNodes(root, root.val) — or Integer.MIN_VALUE; both work, one reads betterWhy it works — why top-down needs no undo4 steps
The distinction between D and E is one of the cleanest in the pattern, and the gate asks for it directly.
- 1
The state travels as a parameter.
dfs(node, maxSoFar)— the value is on the call stack, one copy per frame, invisible to siblings. - 2
Each child gets a freshly computed value.
max(maxSoFar, node.val)produces a new value for the call. The parent's own variable is untouched. - 3
So there is nothing shared to restore. When the call returns, the frame vanishes and with it the child's copy. No cleanup step exists because no mutation happened.
- 4
Contrast with E and F. There the state is a shared list or map. Every mutation on the way down needs an exact inverse on the way up, or one branch's data leaks into the next.
The distinction the gate asks for: sub-variant D needs no backtracking because the state is recomputed per child rather than mutated in a shared structure. E and F mutate, so they must undo.
The seed value is a small modelling choice. LC 1448 can start with root.val or with Integer.MIN_VALUE — both are correct, and one reads better. Being able to say why either works shows you understand what the parameter means.
Depth is the simplest inherited state of all, which is why the DFS form of LC 199 in Pattern 1 D is really this sub-variant wearing a traversal hat.
Walkthrough — LC 1448 — counting good nodes6 steps
A node is good if no node on the path from the root to it is greater. Watch maxSoFar change per branch without any restoration.
| # | Node | maxSoFar in | Good? | Passes down |
|---|---|---|---|---|
| 1 | 3 (root) | 3 | yes (3 >= 3) | 3 |
| 2 | 1 | 3 | no (1 < 3) | 3 |
| 3 | 3 | 3 | yes | 3 |
| 4 | 4 | 3 | yes (4 >= 3) | 4 |
| 5 | 1 | 4 | no | 4 |
| 6 | 5 | 4 | yes (5 >= 4) | 5 |
Four good nodes. Notice rows 2 and 4: the left branch carried maxSoFar = 3 while the right branch carried 4, at the same time, with no interference. Neither branch had to restore anything — each simply received its own value. Hold that state in a shared field instead and the left branch's 3 would leak into the right.
Key observations — what interviewers are listening for4 points
- Name the direction before you write the signature. Does information flow up or down? Up means a return value; down means a parameter. Getting that right first eliminates most of the design.
- The no-undo property comes from immutability, not from luck. Passing a value creates a copy per frame. That single fact is the whole D-versus-E distinction.
- The counter can be a return value or a field. Summing the children's counts keeps the function pure, which is usually the cleaner choice and is worth preferring when it costs nothing.
- Depth-based problems are this sub-variant. Anything phrased as at depth d or on the path so far is inherited state, even when it is presented as a traversal problem.
Common mistakes4 traps
-
Holding the inherited state in a shared field
Symptom: one branch's value leaks into its sibling.
Prevention: Pass it as a parameter. A fresh value per call is what makes the undo unnecessary.
-
Adding backtracking that is not needed
Symptom: harmless but confusing, and it suggests the D/E distinction has not landed.
Prevention: Nothing shared was mutated, so there is nothing to restore.
-
Choosing a seed that excludes the root
Symptom: the root is misclassified, and every count is off by one.
Prevention: Seed with
root.valorInteger.MIN_VALUE, and be able to say why your choice includes the root. -
Returning the state instead of the count
Symptom: the function reports the running max rather than the answer.
Prevention: The state goes down; the count comes up. Two different channels.
Key takeaway
- Trigger: a node's verdict depends on the root path; the answer is a count.
- The mechanism: state travels down as a parameter, recomputed per child.
- No backtracking: nothing shared is mutated, so nothing needs restoring.
- Contrast: E and F mutate shared structures and therefore must undo.
- Gate: LC 1448 blind, plus why D needs no backtracking while E does. See §5.2.
E Root-to-leaf paths with backtracking#
Keep one mutable path and edit it as you walk. Every push on the way down needs an exact inverse on the way up — otherwise one branch's nodes leak into the next.
Mental model
“There is a single list representing where I am right now. I add myself entering, and I remove myself leaving. If I ever forget the removal, my sibling inherits my ancestors.”
This looks like DFS and is really backtracking. The distinction from sub-variant D is that the state here is shared and mutated, so the walk owes an undo.
Two rules, both non-negotiable, and each has its own failure mode.
n == null and is a leaf are different tests that both exist and both matter. Using the wrong one either counts each root-to-leaf answer twice or accepts half-paths.
Recognition — reach for this when
- The answer is a list of paths, or something computed per complete root-to-leaf path.
- You need the actual sequence of nodes, not just an aggregate.
- A single shared structure is the natural representation of where I am.
- But not when a per-node parameter would do. If the state is one immutable value, use D and skip the undo entirely.
Java113. Two rules, both non-negotiable:13 lines
// 113. Two rules, both non-negotiable:
// 1. exactly one removeLast() for every addLast(), on EVERY exit path
// 2. record a COPY — new ArrayList<>(path) — or all results alias one list that ends empty
// LEAF: (left == null && right == null). Not "n == null", which is one step past a leaf.
void paths(TreeNode n, int rem, LinkedList<Integer> path, List<List<Integer>> out) {
if (n == null) return;
path.addLast(n.val);
rem -= n.val;
if (n.left == null && n.right == null && rem == 0) out.add(new ArrayList<>(path));
paths(n.left, rem, path, out);
paths(n.right, rem, path, out);
path.removeLast();
}Why it works — the two rules, and the leaf condition4 steps
Three details, and each one corresponds to a specific wrong answer people actually produce.
- 1
The path is shared, so it must be restored. One list represents the current root path. Entering a node appends; leaving must remove. The undo goes after all recursive calls, on every exit path.
- 2
Recording must copy. The list keeps changing after you record it. Storing the reference means every result points at the same object, which is empty by the time the walk finishes.
- 3
A leaf is not a null.
left == null && right == nullis a leaf.n == nullis the empty subtree one step beyond it — and recursing into both children of a leaf reaches it twice. - 4
Which is why the tests are not interchangeable. Use
n == nullas the leaf condition and every root-to-leaf answer is counted twice, or half-paths get accepted, depending on where you put the record.
The two rules, stated as one: one undo per mutation, placed after all recursive calls on every exit path — and record new ArrayList<>(path), never the list itself.
The undo has to survive early returns. If any branch returns before the bottom of the function, that path also owes a removeLast. A try/finally, or simply having a single exit point, removes the whole class of bug.
LC 257 and 113 are the same walk with different payloads — a string versus a summed list. The discipline is identical, which is why they are graded as one skill.
Walkthrough — LC 113 — path sum II, watching the undo6 steps
Target 8. Follow the path column: it must return to exactly its previous contents each time a branch finishes.
| # | At | Action | path after | Note |
|---|---|---|---|---|
| 1 | 5 | addLast(5) | [5] | enter root |
| 2 | 4 | addLast(4) | [5, 4] | enter left |
| 3 | 3 | addLast(3) | [5, 4, 3] | leaf, sum 12 != 8 — no record |
| 4 | 3 | removeLast() | [5, 4] | undo, leaving the leaf |
| 5 | 4 | removeLast() | [5] | undo, leaving node 4 |
| 6 | 3 | addLast(3) | [5, 3] | leaf, sum 8 — record new ArrayList<>(path) |
Result [[5, 3]]. Rows 4 and 5 are the whole discipline: without them, row 6 would begin from [5, 4, 3] and produce [5, 4, 3, 3]. And had row 6 recorded path itself instead of a copy, the final answer would be an empty list — the walk empties it on the way out.
Key observations — what interviewers are listening for4 points
- Pair every mutation with its inverse as you type it. Write
addLastandremoveLastin the same keystroke burst, then fill the recursion between them. The bug then cannot be written. - The copy is not defensive programming, it is correctness. The list is guaranteed to change afterwards. Recording a reference records a future empty list.
- State the leaf condition without hesitating. The gate says so explicitly.
left == null && right == null— and know whyn == nullis a different, also-necessary test. - Early returns owe an undo too. The most common leak is a branch that returns before reaching the bottom of the function.
Common mistakes4 traps
-
addLastwithout a matchingremoveLastSymptom: paths from one branch leak into the next.
Prevention: One undo per mutation, placed after all recursive calls, on every exit path.
-
Recording the path list instead of a copy
Symptom: every result is the same list — and it ends up empty.
Prevention:
new ArrayList<>(path)at the moment of recording. -
Treating
n == nullas the leaf conditionSymptom: root-to-leaf answers counted twice, or half-paths accepted.
Prevention: A leaf is
left == null && right == null. Both tests exist and mean different things. -
Putting the undo before the recursive calls
Symptom: the children walk with an incomplete path.
Prevention: The undo is the last statement, after both calls have returned.
Key takeaway
- Trigger: the answer is a list of complete root-to-leaf paths.
- Rule 1: exactly one
removeLastperaddLast, after all recursive calls, on every exit. - Rule 2: record
new ArrayList<>(path)— a copy, never the live list. - Leaf:
left == null && right == null, notn == null. - Gate: LC 113 blind with one undo per mutation, the copy-on-record, and the leaf condition stated without hesitation. See §5.2.
F Prefix sums on the root path#
LC 560 transplanted onto a tree. The array becomes the current root path, and the hash map of prefix sums becomes a map that must be undone on the way back up.
Mental model
“I am running the subarray-sum-equals-K algorithm, except my array is the path from the root to wherever I am standing — and that array shrinks when I step back.”
The transfer is the point of this sub-variant, and the gate asks for it directly: explain LC 437 as LC 560 on the root path.
prefix[s] = how many nodes on the current root path have running sum s. The word current is doing all the work, and it is the undo that makes it true.
Without the decrement, cousins see each other's prefix sums and the count includes paths that do not exist — ones that would have to jump sideways across the tree.
Recognition — reach for this when
- Counting downward paths with a given sum, not necessarily starting at the root.
- You already know the array version of the problem.
- The path is a contiguous run of ancestors — exactly a subarray of the root path.
- But not for paths that bend. A path through a node using both children is sub-variant C, not this.
Java437. This is LC 560 (subarray sum equals K) with the array replaced by the current root path.16 lines
// 437. This is LC 560 (subarray sum equals K) with the array replaced by the current root path.
// prefix[s] = how many nodes on the CURRENT root path have running sum s.
// The undo is what makes "current root path" true; without it, cousins see each other's sums.
Map<Long, Integer> prefix = new HashMap<>();
int total = 0;
void countPaths(TreeNode n, long cur, int target) {
if (n == null) return;
cur += n.val;
total += prefix.getOrDefault(cur - target, 0); // count BEFORE inserting: no zero-length path
prefix.merge(cur, 1, Integer::sum);
countPaths(n.left, cur, target);
countPaths(n.right, cur, target);
prefix.merge(cur, -1, Integer::sum); // UNDO on the way up
}
// seed prefix.put(0L, 1) so that a path starting at the root is countedWhy it works — the transplant, and the two bookkeeping rules4 steps
One idea carried across, and two details that make the carry legitimate.
- 1
The array becomes the root path. In LC 560 the prefix sums come from
a[0..i]. Here they come from the chain of ancestors down to the current node — which is exactly a contiguous sequence, so the same counting works. - 2
The lookup is unchanged.
answer += prefix[p - k]counts how many ancestors have a running sum that makes the segment between them and here equalk. - 3
The seed.
prefix.put(0L, 1)before the first call, so that a path starting at the root is counted. Seeding inside the DFS would re-seed at every node. - 4
The undo. The path shrinks when you return, so the map must shrink with it. Decrementing
prefix[p]is the last statement of the DFS body — otherwise a node's sum stays visible to its cousins.
The sentence that makes current root path true: the decrement is the last statement of the DFS body. Without it, cousins see each other's sums and you count paths that would have to jump sideways across the tree.
Use long for the running sum. LC 437's values and depths can overflow int silently, and the failure looks like an ordinary wrong answer rather than a crash.
The seed goes before the first call, not inside it. Seeding inside means every node re-registers a zero prefix, which inflates the count by exactly the number of nodes.
Walkthrough — LC 437 — counting downward paths summing to 85 steps
Watch the map contents. The entry added when entering a node is gone again by the time its sibling is visited.
| # | At | running sum p | look up p - 8 | count | map after |
|---|---|---|---|---|---|
| 1 | root 5 | 5 | map[-3] = 0 | 0 | {0:1, 5:1} |
| 2 | left 3 | 8 | map[0] = 1 | 1 | {0:1, 5:1, 8:1} |
| 3 | leaf 5 | 13 | map[5] = 1 | 2 | {0:1, 5:1, 8:1, 13:1} |
| 4 | unwind to root | -- | -- | 2 | {0:1, 5:1} — 13 and 8 removed |
| 5 | right 3 | 8 | map[0] = 1 | 3 | {0:1, 5:1, 8:1} |
Three paths: 5-3, 3-5 and the right-hand 5-3. Row 4 is the sub-variant in one line — the left branch's entries are removed before the right branch runs. Skip the decrement and row 5 would still see 8:1 and 13:1 from the left branch, counting a path that steps sideways from one child to the other, which no tree path can do.
Key observations — what interviewers are listening for4 points
- Lead with the transfer. This is LC 560 with the array replaced by the root path. The gate asks for exactly that sentence, and it makes the rest of the solution follow.
- The undo is what defines the data structure. The map claims to describe the current path. That claim is only true because of the decrement — so the decrement is part of the definition, not cleanup.
- Seed once, outside.
{0: 1}before the walk begins. It is what lets a path that starts at the root be counted at all. - Downward only. This counts paths that run straight down. A path that bends through a node is a different question and a different sub-variant.
Common mistakes4 traps
-
Prefix map not decremented on the way up
Symptom: overcounts paths that appear to span two branches.
Prevention: The decrement is the last statement of the DFS body.
-
Prefix map missing the
{0: 1}seedSymptom: misses every path that starts at the root.
Prevention: Seed before the first call, not inside it.
-
Seeding inside the DFS
Symptom: the count is inflated by roughly the number of nodes.
Prevention: One seed, before the walk starts.
-
intrunning sumsSymptom: silent overflow on deep or large-valued trees.
Prevention: Accumulate in
long.
Key takeaway
- Trigger: count downward paths with a given sum, starting anywhere.
- The transfer: LC 560 with the array replaced by the current root path.
- Seed:
prefix.put(0L, 1)once, before the walk. - Undo: decrement
prefix[p]as the last statement of the DFS body. - Gate: explain LC 437 as LC 560 on the root path, including why the entry must be decremented. See §5.2.
G Lowest common ancestor#
Six lines, one real proof. Ask both children did you find anything? — and the pattern of answers tells you whether this node is the split point.
Mental model
“If both sides come back non-null, the two targets are in different subtrees, so I am the place they meet. If only one side comes back, I pass it up unchanged — and that turns out to be right whether it is the answer or just one of the targets.”
The code is trivial and the argument is not. What makes it work is that a single non-null return is correct under two different readings, and you never have to distinguish them.
That double meaning is the thing to be able to defend, because it is exactly what an interviewer will probe.
You never need to know which of the two readings applies. That is what collapses the whole problem into six lines.
Recognition — reach for this when
- Lowest common ancestor, meeting point, or where do these two paths converge.
- The tree is a plain binary tree with no ordering to exploit.
- But not on a BST, where the ordering gives an
O(h)walk with no recursion — that is Pattern 3 A. - And not when a target may be absent, unless you extend the return value. See below.
Java236. Six lines, one real proof.11 lines
// 236. Six lines, one real proof.
// If both sides return non-null, p and q are in different subtrees ⇒ n is the split point.
// If only one side returns non-null, it is either the LCA found deeper, or one target that is
// an ancestor of the other — and passing it up is correct in both readings.
TreeNode lca(TreeNode n, TreeNode p, TreeNode q) {
if (n == null || n == p || n == q) return n;
TreeNode l = lca(n.left, p, q);
TreeNode r = lca(n.right, p, q);
if (l != null && r != null) return n;
return l != null ? l : r;
}Why it works — the one step worth defending4 steps
Two of the three cases are obvious. The third is the whole problem.
- 1
The base.
nullreturnsnull; a node equal toporqreturns itself. Nothing subtle yet. - 2
Both sides non-null. One target was found somewhere left and the other somewhere right. No node below
ncan contain both, sonis the lowest node that does — returnn. - 3
Exactly one side non-null, reading (a). The LCA was already determined deeper in that subtree. Passing it up unchanged preserves it, because no ancestor can be lower.
- 4
Exactly one side non-null, reading (b). The value returned is one target, and the other lies beneath it. Then that target is itself the LCA — so passing it up is again correct. The code never distinguishes the two, and does not need to.
The step the gate asks you to defend: if only one side returns non-null, it is either the LCA found deeper or one target that is an ancestor of the other — and passing it up is correct in both readings.
This version assumes both targets exist. If they might not, you must return (found_p, found_q, node) or run a containment check first — a standard follow-up, and a place where it worked on LeetCode is not an answer.
Say the assumption out loud before you rely on it. Unstated, it is the difference between a correct solution and one that confidently returns a node when one target was never in the tree.
Walkthrough — LC 236 — both readings in one tree6 steps
Targets p = 5 and q = 4, where 4 sits beneath 5. This is reading (b), the case people find hardest to justify.
| # | Node | left returns | right returns | Verdict |
|---|---|---|---|---|
| 1 | 6 | null | null | returns null |
| 2 | 7 | null | null | null |
| 3 | 4 | -- | -- | is a target -> returns 4 |
| 4 | 2 | null | 4 | one side -> pass up 4 |
| 5 | 5 | null (from 6) | 4 (from 2) | is a target itself -> returns 5 immediately |
| 6 | 3 | 5 | null | one side -> pass up 5 -> answer 5 |
Answer 5. Row 5 is reading (b) in action: node 5 is one of the targets and the other is beneath it, so 5 short-circuits and returns itself without ever looking at what came up from node 2. Row 6 then passes it along unchanged. At no point did the code decide which reading it was in.
Key observations — what interviewers are listening for4 points
- Defend the one-sided case unprompted. Both readings, in one sentence. That is exactly what the gate names, and it is the only part of this problem that is not mechanical.
- State the existence assumption. This assumes both targets are present. Saying it before being asked converts a hidden bug into a deliberate scope decision.
- The short-circuit at a target is deliberate. Returning immediately on
n == p || n == qis what makes reading (b) work without extra logic. - On a BST, do not use this. The ordering lets you walk down in
O(h)with no recursion at all. Reaching for the general algorithm on a BST misses the point of the constraint.
Common mistakes4 traps
-
Assuming both targets exist without saying so
Symptom: wrong answer when one is absent — a node is returned that is not an ancestor of anything.
Prevention: Say the assumption out loud; if it is unstated in the problem, return a found-flag pair.
-
Trying to distinguish the two one-sided readings
Symptom: extra state, extra branches, and no change in behaviour.
Prevention: Passing the value up is correct in both. That is the point.
-
Continuing to search below a found target
Symptom: correct but wasteful, and it breaks the reading-(b) shortcut.
Prevention: Return immediately when the node is a target.
-
Using this on a BST
Symptom:
O(n)whereO(h)was available.Prevention: On a BST, compare values and walk down. See Pattern 3 A.
Key takeaway
- Trigger: lowest common ancestor in a plain binary tree.
- Both non-null: this node is the split point.
- One non-null: pass it up — correct whether it is the LCA or an ancestor target.
- Caveat: assumes both targets exist; otherwise return a found-flag pair.
- Gate: LC 236 blind, defending the both-sides-non-null step and the ancestor case. See §5.2.
H Construction from traversals#
One traversal tells you who the root is; the other tells you how many nodes go left. Together they rebuild the tree, and neither can do it alone.
Mental model
“Preorder hands me the next root. I look it up in the inorder sequence, and everything to its left in that sequence is its left subtree. Now I know both sizes and can recurse.”
The pair is what carries the information: preorder supplies identity, inorder supplies split position. Either alone is ambiguous.
LC 105 and 106 differ by exactly one thing, and the gate asks you to state it from memory after writing both back to back.
The cursor must be shared because consuming a node in one subtree has to be visible to the other. Pass it by value and every sibling restarts from the same position.
Recognition — reach for this when
- You are given two traversals and asked to rebuild the tree.
- Values are distinct, so the inorder lookup is unambiguous.
- But not with preorder alone, or inorder alone — neither is uniquely decodable.
- And not with duplicate values, where the inorder position cannot be located reliably.
Java105. Preorder gives the root; inorder says how many nodes belong to the left subtree.19 lines
// 105. Preorder gives the root; inorder says how many nodes belong to the left subtree.
// The shared cursor `pre` must be a field: copying it into each frame breaks the ordering.
int pre = 0;
Map<Integer, Integer> pos = new HashMap<>(); // value -> index in inorder
TreeNode buildTree(int[] preorder, int[] inorder) {
for (int i = 0; i < inorder.length; i++) pos.put(inorder[i], i);
return build(preorder, 0, inorder.length - 1);
}
TreeNode build(int[] preorder, int lo, int hi) {
if (lo > hi) return null; // empty range, not "lo == hi"
int v = preorder[pre++];
TreeNode n = new TreeNode(v);
int m = pos.get(v);
n.left = build(preorder, lo, m - 1); // LEFT first: preorder emits left before right
n.right = build(preorder, m + 1, hi);
return n;
}Java106. Postorder is consumed FROM THE RIGHT, so the right subtree must be built FIRST.13 lines
// 106. Postorder is consumed FROM THE RIGHT, so the right subtree must be built FIRST.
// That single swap is the entire difference from 105. Write them back to back.
int post; // init to postorder.length - 1
TreeNode buildPost(int[] postorder, int lo, int hi, Map<Integer, Integer> pos) {
if (lo > hi) return null;
int v = postorder[post--];
TreeNode n = new TreeNode(v);
int m = pos.get(v);
n.right = buildPost(postorder, m + 1, hi, pos);
n.left = buildPost(postorder, lo, m - 1, pos);
return n;
}Why it works — why two traversals, and why the cursor is shared4 steps
Two structural facts. The second is where the bug lives.
- 1
Preorder supplies identity. Its first element is the root of the current subtree. Consuming left to right hands you each subtree's root in exactly the order you need it.
- 2
Inorder supplies the split. Find the root's position in the inorder range; everything before it is the left subtree, everything after is the right. That gives both sizes.
- 3
The cursor must be shared. Building the left subtree consumes an unknown number of preorder entries, and the right subtree must start after all of them. A per-frame copy cannot know that, so it rebuilds from the wrong slice.
- 4
Postorder reverses the order of construction. Read from the right, postorder gives root, then right subtree, then left. So LC 106 must build the right subtree first — the one swap that separates it from LC 105.
The single difference between LC 105 and LC 106: postorder is consumed from the right, so the right subtree must be built first. Write them back to back and state that difference from memory.
A hash map from value to inorder index turns the lookup from O(n) into O(1), taking the whole construction from O(n^2) to O(n). Worth building unprompted, and worth saying why.
Mirroring is a silent failure. Build the left subtree first in LC 106 and you get a perfectly valid tree that is the mirror of the right answer — no exception, no crash.
Walkthrough — LC 105 — rebuilding from preorder and inorder5 steps
preorder = [3, 9, 20, 15, 7], inorder = [9, 3, 15, 20, 7]. The cursor advances once per node, across all frames.
| # | Cursor takes | Inorder range | Left size | Builds |
|---|---|---|---|---|
| 1 | 3 | [9, 3, 15, 20, 7] | 1 (just 9) | root 3; recurse left on [9] |
| 2 | 9 | [9] | 0 | leaf 9; both sides empty |
| 3 | 20 | [15, 20, 7] | 1 (just 15) | node 20 — the cursor is now at index 2, which only works because frame 2 advanced it |
| 4 | 15 | [15] | 0 | leaf 15 |
| 5 | 7 | [7] | 0 | leaf 7 |
Rebuilt correctly. Row 3 is the reason the cursor cannot be copied: node 20's frame relies on frames 1 and 2 having already consumed 3 and 9. Hand each frame its own copy and node 20 would read 9 instead, building a completely different tree with no error raised.
Key observations — what interviewers are listening for4 points
- Name what each traversal contributes. Preorder gives identity, inorder gives the split. One sentence that makes the algorithm obvious and explains why one traversal is not enough.
- Write 105 and 106 back to back, deliberately. The gate asks for exactly this, because the difference is one line and it will not stick otherwise.
- Build the index map without being asked.
O(n)versusO(n^2), for three lines. Explaining the improvement is as valuable as making it. - A mirrored tree throws no exception. That is what makes the LC 106 ordering bug dangerous — it produces a plausible tree, silently.
Common mistakes4 traps
-
Copying the
precursor into each frameSymptom: subtrees built from the wrong slice; the result is a different tree entirely.
Prevention: The cursor is shared state: a field, or an
int[1]. -
Building the left subtree first in LC 106
Symptom: a mirrored tree, and no exception thrown.
Prevention: Postorder is consumed from the right, so the right subtree is built first.
-
Scanning the inorder array for each root
Symptom:
O(n^2)on skewed input.Prevention: Precompute a value-to-index map once.
-
Assuming distinct values
Symptom: the inorder lookup becomes ambiguous and the reconstruction is arbitrary.
Prevention: Check the constraint. With duplicates, the pair of traversals is not uniquely decodable.
Key takeaway
- Trigger: rebuild a tree from two traversals.
- The pairing: preorder gives the root; inorder gives the left-subtree size.
- LC 106: postorder is read from the right, so build the right subtree first.
- The cursor: shared — a field or
int[1], never copied per frame. - Gate: LC 105 blind, then LC 106 immediately after, stating the one difference from memory. See §5.2.
I Serialization#
The null markers are the structure. Preorder with markers is uniquely decodable; preorder alone is not, and inorder is not even with them.
Mental model
“I am writing down the tree so that reading it back is deterministic. The nulls are not padding — they are the only thing that tells the reader where a subtree stops.”
Sub-variant H needed two traversals because one was ambiguous. This sub-variant removes the ambiguity a different way: by recording the absences.
Understanding why inorder fails even with markers is the part the gate asks for, and it is a genuinely instructive failure.
Preorder works because the root comes first, so the reader always knows what it is looking at. Inorder never establishes that, with or without markers.
Recognition — reach for this when
- Encode a tree to a string and decode it back exactly.
- Structural identity — are these two subtrees the same shape and values?
- Finding duplicate subtrees, which is canonical-form plus a hash map.
- But not inorder, in any form. It cannot locate the root, so it cannot be decoded.
Java297. The null markers ARE the structure. Preorder + markers is uniquely decodable;27 lines
// 297. The null markers ARE the structure. Preorder + markers is uniquely decodable;
// preorder alone is not, and inorder is not even with markers (it cannot locate the root).
String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
ser(root, sb);
return sb.toString();
}
void ser(TreeNode n, StringBuilder sb) {
if (n == null) { sb.append("#,"); return; }
sb.append(n.val).append(',');
ser(n.left, sb);
ser(n.right, sb);
}
TreeNode deserialize(String data) {
return des(new ArrayDeque<>(Arrays.asList(data.split(","))));
}
TreeNode des(Deque<String> q) { // ONE shared cursor, same order as writing
String t = q.poll();
if (t == null || t.equals("#")) return null;
TreeNode n = new TreeNode(Integer.parseInt(t));
n.left = des(q);
n.right = des(q);
return n;
}Why it works — why preorder plus markers, and nothing else4 steps
Three encodings, one of which works. The reasons the other two fail are the content here.
- 1
Preorder alone is ambiguous.
3, 9, 20could be a left chain, a right chain, or a root with two children. Nothing says where a subtree ends. - 2
Markers supply the boundaries. A
#says this subtree is empty, which is exactly the missing information. Now every recursive call knows when to stop. - 3
Preorder makes the reader's job deterministic. The next unread token is always the root of the next subtree, so decoding mirrors encoding exactly — read root, decode left, decode right.
- 4
Inorder cannot work even with markers. The root sits somewhere in the middle of its own encoding, and nothing identifies which token it is. Without knowing the root you cannot split, and without splitting you cannot recurse.
The claim the gate asks you to explain: the null markers are the structure. Preorder plus markers is uniquely decodable; preorder alone is not, and inorder is not even with markers, because it cannot locate the root.
Deserialization uses one shared cursor, exactly as construction did in sub-variant H, and for exactly the same reason: the left subtree consumes an unknown number of tokens.
Structural identity is serialization plus a map. LC 652 canonicalises every subtree to a string and counts them — a direct reuse, and a good sign the sub-variant has landed.
Delimiters matter more than they look. 1,2 and 12 must not collide, so separate tokens explicitly rather than concatenating digits.
Walkthrough — serializing and reading back a four-node tree5 steps
Encoding is a plain preorder walk that emits # for null. Decoding reads the same tokens in the same order.
| # | Encode step | Emits | Decode step | Builds |
|---|---|---|---|---|
| 1 | visit 1 | 1 | read 1 | root 1, then decode its left |
| 2 | visit 2, both children null | 2, #, # | read 2, #, # | leaf 2 — both # end it |
| 3 | visit 3, left is 4 | 3 | read 3 | node 3; the next token belongs to its left |
| 4 | visit 4, both null | 4, #, # | read 4, #, # | leaf 4 |
| 5 | 3's right is null | # | read # | 3's right is empty — subtree complete |
Encoded as 1,2,#,#,3,4,#,#,#. Row 5 is the marker earning its place: without that final # the decoder cannot tell whether node 3 has a right child, and the encoding would describe several different trees. Every # removes exactly one ambiguity.
Key observations — what interviewers are listening for4 points
- The markers are data, not padding. Saying the nulls are the structure is the whole insight, and it is what the gate is checking.
- Be able to reject inorder with a reason. It cannot locate the root. That is a sharper answer than it does not work, and it demonstrates you understand what decoding requires.
- The shared cursor recurs. Same mechanism as sub-variant H. Noticing the reuse is worth more than the individual problems.
- Canonical strings enable subtree comparison. Once a subtree has a unique encoding, are these equal and find duplicates both become map lookups.
Common mistakes4 traps
-
Serialising preorder without null markers
Symptom: the string describes many different trees; decoding is arbitrary.
Prevention: The markers are the structure. Emit one per absent child.
-
Trying to use inorder
Symptom: the decoder cannot find the root and the whole approach collapses.
Prevention: Preorder puts the root first, which is what makes decoding deterministic.
-
Copying the cursor during deserialization
Symptom: subtrees decoded from the wrong offset.
Prevention: One shared cursor, exactly as in sub-variant H.
-
Concatenating values without a delimiter
Symptom:
1,2and12become indistinguishable.Prevention: Emit explicit separators between tokens.
Key takeaway
- Trigger: encode and decode a tree, or compare subtrees structurally.
- The encoding: preorder plus null markers — uniquely decodable.
- Why not inorder: it cannot locate the root, with or without markers.
- Decoding: one shared cursor, root first, then left, then right.
- Gate: explain why preorder needs markers and inorder cannot work, then write serialize/deserialize with one shared cursor. See §5.2.
J In-place restructuring#
Recurse first, rewire after. Mutating a node before its children have been processed means they get processed in a shape you already changed.
Mental model
“I want to move pointers around, but my children are still the old shape. So I let the recursion finish, and only then do I rewire — otherwise I am inverting things twice.”
The order-of-operations rule is the whole sub-variant, and it shows up in three quite different problems: inverting, flattening, and linking a level.
The O(1)-space variants are where it gets interesting, because they replace recursion with a pointer discipline that has to be exactly right.
LC 117's trick is that you do not need a queue at all — the next pointers you built on the previous level give you a ready-made iteration order.
Recognition — reach for this when
- The tree itself is the output — inverted, flattened, or linked.
- An
O(1)-space constraint on a problem that looks like it needs a queue. - Pointer rewiring rather than value computation.
- But not when the original tree must survive. These mutate in place by definition.
Java226. Recurse first, rewire after. Swapping before the calls inverts subtrees twice.7 lines
// 226. Recurse first, rewire after. Swapping before the calls inverts subtrees twice.
TreeNode invert(TreeNode n) {
if (n == null) return null;
TreeNode l = invert(n.left), r = invert(n.right);
n.left = r; n.right = l;
return n;
}Java114. Flatten, O(1) space. For each node, splice the left subtree in between the node and15 lines
// 114. Flatten, O(1) space. For each node, splice the left subtree in between the node and
// its right subtree; the rightmost node of the left subtree inherits the old right chain.
void flatten(TreeNode root) {
TreeNode cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode pred = cur.left;
while (pred.right != null) pred = pred.right;
pred.right = cur.right;
cur.right = cur.left;
cur.left = null;
}
cur = cur.right;
}
}Java117. O(1)-space level order: the level you have ALREADY linked is the queue for the next one.14 lines
// 117. O(1)-space level order: the level you have ALREADY linked is the queue for the next one.
// The dummy head removes every "is this the first child on the level?" special case.
Node connect(Node root) {
Node cur = root;
while (cur != null) {
Node dummy = new Node(0), tail = dummy;
for (Node n = cur; n != null; n = n.next) {
if (n.left != null) tail = tail.next = n.left;
if (n.right != null) tail = tail.next = n.right;
}
cur = dummy.next;
}
return root;
}Why it works — why the recursion comes first4 steps
One ordering rule, and two pointer disciplines that depend on it.
- 1
Mutation invalidates what the children saw. If you swap
leftandrightbefore recursing, the recursive calls operate on subtrees that have already moved — and invert them a second time. - 2
So the calls go first. Recurse into both children, let them finish restructuring themselves, and only then rewire the current node's pointers.
- 3
LC 114 splices rather than rebuilds. Put the left subtree between the node and its right subtree. The rightmost node of the left subtree is the one that inherits the old right chain — find it, attach, then clear
left. - 4
LC 117 reuses the previous level as its queue. Once a level is linked by
next, walking it gives you every parent in order, so you can link the level below without any auxiliary structure. The dummy head means the first child of a level needs no special case.
The rule that covers all three problems: recurse, then rewire. Swapping before the calls inverts subtrees twice, and in some shapes the tree comes back apparently unchanged.
The dummy head is the same idiom as the linked-list one. It exists so that is this the first element? never needs asking — and reaching for it unprompted is a small, strong signal.
LC 114's O(1) version is worth having over the recursive one, because the whole point of the problem is the space constraint. The gate asks specifically for that version.
Walkthrough — LC 114 — flattening by splicing5 steps
At each node with a left subtree: find the rightmost node of that left subtree, hand it the current right chain, then move the left subtree across.
| # | At | Left subtree | Rightmost of it | Rewire |
|---|---|---|---|---|
| 1 | node 1 | 2 -> (3, 4) | 4 | 4.right = 5; 1.right = 2; 1.left = null |
| 2 | node 2 | 3 | 3 | 3.right = 4; 2.right = 3; 2.left = null |
| 3 | node 3 | none | -- | move on to 3.right |
| 4 | node 4 | none | -- | move on |
| 5 | nodes 5, 6 | none | -- | already a chain |
Result 1 -> 2 -> 3 -> 4 -> 5 -> 6, all on right pointers with every left cleared. Row 1 is the splice: node 4 is the rightmost of node 1's left subtree, so it is the node that must inherit the old right chain starting at 5. Attach it anywhere else and the tail is lost.
Key observations — what interviewers are listening for4 points
- State the ordering rule before writing. Recurse, then rewire. It covers inverting, flattening and linking, and it is the only bug worth worrying about in the recursive forms.
- Double inversion can look like success. On symmetric trees, swapping before recursing returns the original tree unchanged — which passes a careless test.
- The rightmost node is the attachment point. In LC 114 it is the only node whose
rightis free, which is why it inherits the old chain. - LC 117 needs no queue at all. The level I already linked is the queue for the next one. That reframing is what makes the
O(1)space possible.
Common mistakes4 traps
-
Swapping children before recursing in LC 226
Symptom: double inversion — and in some shapes the tree comes back unchanged.
Prevention: Recurse, then rewire.
-
Attaching the old right chain to the wrong node in LC 114
Symptom: the tail is lost and the flattened list is truncated.
Prevention: The rightmost node of the left subtree inherits it — that is the only node with a free
right. -
Forgetting to clear
leftafter splicingSymptom: the result is not a right-only chain, so it fails the problem's definition.
Prevention:
node.left = nullas part of every splice. -
Handling the first child of a level as a special case in LC 117
Symptom: branchy, error-prone code.
Prevention: Use a dummy head; the special case disappears.
Key takeaway
- Trigger: the tree itself is the output — inverted, flattened, linked.
- The rule: recurse, then rewire.
- LC 114: splice the left subtree in; its rightmost node inherits the old right chain.
- LC 117: the linked level is the queue for the next; a dummy head kills the first-child case.
- Gate: LC 114's
O(1)-space version and LC 117's dummy-head loop, both blind. See §5.2.
K Tree DP with per-child states#
Dynamic programming that happens to run on a tree. The state is per node, the transition is over children, and the order is postorder — because that is the only topological order available.
Mental model
“One value per node is not enough. I return a small tuple — one entry per state — and the parent combines the tuples rather than the numbers.”
The design work is entirely up front: name the states, name the transition, and name what null returns for each state separately. Getting the null value wrong is the classic failure, and it shows up as an off-by-one at every leaf.
LC 968 is called out as the hardest gate in the document, and the reason is that all three parts have to be designed rather than recalled.
Each problem's null value is a separate design decision, made per state. null = 1 in LC 968 is not a convention — it is the claim that an absent child needs no covering.
Recognition — reach for this when
- One number per node is not enough — a node's best answer depends on a choice it makes.
- Words like rob, cover, choose, cannot both, with a constraint between parent and child.
- You can enumerate a small fixed set of states per node.
- But not when every node needs its own global answer. That is rerooting, sub-variant L.
Java337. Return one entry per state: {rob this node, do not rob this node}.10 lines
// 337. Return one entry per state: {rob this node, do not rob this node}.
// null returns the identity for BOTH states — an empty subtree contributes nothing either way.
int[] rob(TreeNode n) {
if (n == null) return new int[]{0, 0};
int[] l = rob(n.left), r = rob(n.right);
int with = n.val + l[1] + r[1]; // children must be skipped
int without = Math.max(l[0], l[1]) + Math.max(r[0], r[1]); // children choose freely
return new int[]{with, without};
}
// answer: Math.max(rob(root)[0], rob(root)[1])Java979. The return value is a SURPLUS and may be negative. What you count is flow across an10 lines
// 979. The return value is a SURPLUS and may be negative. What you count is flow across an
// edge, not nodes: |surplus| coins must cross the edge to the parent, in either direction.
int moves = 0;
int surplus(TreeNode n) {
if (n == null) return 0;
int l = surplus(n.left), r = surplus(n.right);
moves += Math.abs(l) + Math.abs(r);
return n.val + l + r - 1; // this node keeps exactly one coin
}Java968. Three states, greedy postorder. Place a camera as LATE as possible — at the parent of13 lines
// 968. Three states, greedy postorder. Place a camera as LATE as possible — at the parent of
// an uncovered node — because a camera at the parent covers strictly more.
// 0 = uncovered 1 = covered, no camera here 2 = has a camera
// null MUST be 1: an absent child needs no cover, otherwise every leaf gets a camera.
int cameras = 0;
int cover(TreeNode n) {
if (n == null) return 1;
int l = cover(n.left), r = cover(n.right);
if (l == 0 || r == 0) { cameras++; return 2; } // a child is exposed ⇒ camera HERE
return (l == 2 || r == 2) ? 1 : 0;
}
// answer: cameras + (cover(root) == 0 ? 1 : 0) — the root can end up uncoveredWhy it works — designing the state, the transition and the null value4 steps
Three design decisions, in order. The code is short once they are settled and unwriteable before.
- 1
Name the states. What choice does a node make? LC 337: robbed or not. LC 968: uncovered, covered, or holding a camera. The state set must be small and exhaustive.
- 2
Name the transition. How does a parent's state depend on its children's? This is where the constraint lives — if I rob this node I cannot rob its children, a camera here covers my children and my parent.
- 3
Name the null value, per state. Ask what the empty subtree contributes to each state individually. In LC 968 an absent child needs no cover, so
nullmust be state 1 — return 0 instead and every leaf gets a camera it does not need. - 4
Postorder is forced. Children's tuples must exist before the parent combines them, and postorder is the only topological order a tree traversal gives you.
The design decision that fails silently: ask what the empty subtree contributes to each state individually. In LC 968 null must be 1 — an absent child needs no cover — otherwise every leaf gets a camera.
LC 979 counts flow, not nodes. The return value is a surplus and may be negative; |surplus| coins must cross the edge to the parent, in either direction. Counting nodes instead of edge traffic is the wrong model and produces a plausible wrong number.
**LC 968's greedy is *place the camera as late as possible*** — at the parent of an uncovered node, because a camera at the parent covers strictly more than one at the child. That argument is what makes the greedy correct rather than merely reasonable.
The root needs a final check. cameras + (cover(root) == 0 ? 1 : 0) — the root has no parent to cover it, so it can finish uncovered.
Walkthrough — LC 337 — the two-state tuple5 steps
Each node returns {rob, skip}. A robbed node forces both children to be skipped; a skipped node lets each child take its own better option.
| # | Node | children return | rob = val + sum(child skips) | skip = sum(max of child) |
|---|---|---|---|---|
| 1 | leaf 3 | -- | 3 | 0 |
| 2 | leaf 1 | -- | 1 | 0 |
| 3 | 2 | {3, 0} | 2 + 0 = 2 | max(3,0) = 3 |
| 4 | right 3 | {1, 0} | 3 + 0 = 3 | max(1,0) = 1 |
| 5 | root 3 | {2,3} and {3,1} | 3 + 3 + 1 = 7 | max(2,3) + max(3,1) = 6 |
Answer max(7, 6) = 7. Row 5 shows why one number per node would not do: the root's rob value needs its children's skip entries specifically, not their best. Collapse the tuple to a single best-so-far and that information is gone.
Key observations — what interviewers are listening for5 points
- Design the three parts before writing any code. States, transition, null-per-state. The gate for LC 968 asks you to do exactly that from scratch, without recalling the code.
- The null value is per state, not per problem. Asking what does the empty subtree contribute to this state separately for each entry is what prevents the leaf off-by-one.
- Justify the greedy, do not assert it. A camera at the parent covers strictly more than one at the child is why placing late is optimal. Without that sentence the solution is a guess that happens to pass.
- Watch for surplus-style returns. LC 979's negative values confuse people because they expect counts. Naming it flow across an edge fixes the model immediately.
- The root often needs a post-check. It has no parent, so any state that relies on the parent to resolve must be handled explicitly after the recursion returns.
Common mistakes4 traps
-
Wrong identity for
nullin a DP stateSymptom: off-by-one at every leaf — LC 968 gives a camera to each one.
Prevention: Ask what the empty subtree contributes to each state, individually.
-
Returning one number instead of a tuple
Symptom: the parent cannot distinguish the child's options, so the constraint cannot be enforced.
Prevention: One entry per state. The tuple is the whole technique.
-
Counting nodes rather than edge flow in LC 979
Symptom: a plausible number that is not the answer.
Prevention: The return value is a surplus;
|surplus|coins cross the edge, in either direction. -
Forgetting the root's final adjustment in LC 968
Symptom: the root can end uncovered and the count is one short.
Prevention:
cameras + (cover(root) == 0 ? 1 : 0).
Key takeaway
- Trigger: a per-node choice with a constraint between parent and child.
- Design first: states, transition, and the
nullvalue per state. - Return: a small tuple, one entry per state — never a single collapsed best.
- LC 968: place cameras as late as possible;
nullis covered; check the root at the end. - Gate: design LC 968's three states, transition and
nullvalue from scratch — the hardest gate in the document. See §5.2.
L Rerooting, two passes#
Every node needs its own global answer, and each answer depends on the whole rest of the tree. One DFS cannot do that — so compute the root's answer, then move the root one edge at a time.
Mental model
“I know the answer for the root. Now, if I shift the root to one of its children, which distances got shorter and which got longer? Everything in that child's subtree came one step closer; everything else moved one step further.”
This is the only sub-variant where a single DFS is provably insufficient, and recognising that is the signature: every node needs its own answer means two passes, not a cleverer one.
The whole content of the problem is one line of arithmetic, and the gate asks you to derive it on a blank page.
Each term counts something concrete: - size[c] is the subtree that came closer, + (n - size[c]) is everything else that moved away. The formula is a bookkeeping identity, not a trick.
Recognition — reach for this when
- Every node needs its own answer, and each depends on the entire tree.
- A single DFS would be
O(n^2)— one full traversal per node. - Moving the root by one edge changes the answer by a computable delta.
- But not when only the root's answer is wanted. Then one postorder pass is enough and rerooting is overhead.
Java834. Pass 1 (postorder): size[v], and ans[root] = sum of distances from the root.24 lines
// 834. Pass 1 (postorder): size[v], and ans[root] = sum of distances from the root.
// Pass 2 (preorder): move the root one edge, from p to c.
// size[c] nodes get one step CLOSER, the other n - size[c] get one step FURTHER:
// ans[c] = ans[p] - size[c] + (n - size[c]) = ans[p] + n - 2 * size[c]
// Derive that line on paper. It is the only content of the problem.
int n;
int[] size, ans;
List<List<Integer>> g;
void down(int v, int p) {
size[v] = 1;
for (int w : g.get(v)) if (w != p) {
down(w, v);
size[v] += size[w];
ans[v] += ans[w] + size[w];
}
}
void up(int v, int p) {
for (int w : g.get(v)) if (w != p) {
ans[w] = ans[v] + n - 2 * size[w];
up(w, v);
}
}Why it works — why one pass cannot work, and the rerooting identity4 steps
First the impossibility, then the four-line derivation the gate asks for.
- 1
Why one DFS fails. A node's answer depends on nodes both below and above it. A single postorder pass only ever knows about the subtree, so it can compute the root's answer and nothing else.
- 2
Pass 1 collects what is local. A postorder walk gives
size[v]for every node, and accumulatesans[root], the sum of distances from the root. - 3
Pass 2 moves the root by one edge. Going from parent
pto childc: every node insidec's subtree is now one step closer, and every node outside it is one step further away. - 4
Which is the identity.
ans[c] = ans[p] - size[c] + (n - size[c]), and simplifying givesans[c] = ans[p] + n - 2 * size[c]. A preorder pass propagates it to the whole tree inO(n).
Derive this on a blank page, and say what each term counts: ans[c] = ans[p] + n - 2 * size[c] — size[c] nodes came one step closer, the other n - size[c] moved one step further.
The direction of the passes is not interchangeable. Pass 1 must be postorder because sizes aggregate upward; pass 2 must be preorder because each child's answer is derived from its parent's.
If the derivation will not come, draw a five-node tree and move the root by hand. The gate says this explicitly — the obstacle is almost never the algebra.
Walkthrough — LC 834 — moving the root one edge5 steps
A six-node tree, n = 6, rooted at 0 with ans[0] = 8. Watch the formula applied to each child in turn.
| # | Move root | size[c] | closer | further | ans[c] |
|---|---|---|---|---|---|
| 1 | 0 -> 1 | 1 | 1 node | 5 nodes | 8 + 6 - 2(1) = 12 |
| 2 | 0 -> 2 | 4 | 4 nodes | 2 nodes | 8 + 6 - 2(4) = 6 |
| 3 | 2 -> 3 | 1 | 1 node | 5 nodes | 6 + 6 - 2(1) = 10 |
| 4 | 2 -> 4 | 1 | 1 node | 5 nodes | 10 |
| 5 | 2 -> 5 | 1 | 1 node | 5 nodes | 10 |
Answers [8, 12, 6, 10, 10, 10]. Row 2 is the interesting one: moving the root towards the bigger subtree lowers the total, because four nodes got closer and only two got further. Row 1 moves towards a leaf and the total rises for the mirror-image reason. The formula is just that trade, written down.
Key observations — what interviewers are listening for4 points
- The recognition is the hard part. Every node needs its own answer is the signature of rerooting. Spotting it before writing an
O(n^2)solution is what the sub-variant is for. - Derive the identity, never recall it. The gate asks for a blank-page derivation and for what each term counts. The second half is what shows it is understood.
- The pass directions are forced. Postorder up for sizes, preorder down for answers. Neither can be swapped, and saying why is a clean way to show you see the dependency structure.
- Drawing beats algebra when stuck. Five nodes, move the root by hand, count. The formula falls out and then stays.
Common mistakes4 traps
-
One DFS for a every node needs its own answer problem
Symptom:
O(n^2)and a time-limit failure.Prevention: That is the signature of rerooting: two passes.
-
Getting the sign wrong in the identity
Symptom: answers drift further from correct the deeper you go.
Prevention:
- size[c]for the subtree that came closer,+ (n - size[c])for everything else. -
Running pass 2 in postorder
Symptom: a child's answer is computed before its parent's exists.
Prevention: Pass 2 is preorder — the parent's answer is the input to the child's.
-
Forgetting that
sizeincludes the node itselfSymptom: off-by-one in every delta.
Prevention:
size[v]countsvplus all its descendants.
Key takeaway
- Trigger: every node needs its own answer, each depending on the whole tree.
- Two passes: postorder for
size[]andans[root]; preorder to propagate. - The identity:
ans[c] = ans[p] + n - 2 * size[c]. - What it counts:
size[c]nodes came closer;n - size[c]moved further. - Gate: derive that line on a blank page and say what each term counts. See §5.2.
2.4 Failure Modes — Tree Recursion#
| # | Bug | Symptom | Prevention |
|---|---|---|---|
| 1 | Returning the recorded value in 543 / 124 | Plausible but wrong on any tree where the best path bends below the root | Say it out loud before coding: record the bend, return the continuation. |
| 2 | Omitting Math.max(0, child) in 124 | Wrong on all-negative trees | A branch with a negative total is never taken. The clamp encodes that. |
| 3 | Treating n == null as the leaf condition | Root-to-leaf answers counted twice, or half-paths accepted | A leaf is left == null && right == null. Both tests exist and mean different things. |
| 4 | addLast without a matching removeLast | Paths from one branch leak into the next | One undo per mutation, placed after all recursive calls, on every exit path. |
| 5 | Recording the path list instead of a copy | Every result is the same (empty) list | new ArrayList<>(path) at the moment of recording. |
| 6 | Prefix map not decremented on the way up | Overcounts paths that span two branches | The decrement is the last statement of the DFS body. |
| 7 | Prefix map missing the {0: 1} seed | Misses every path that starts at the root | Seed before the first call, not inside it. |
| 8 | Copying the pre cursor into each frame in 105 | Subtrees built from the wrong slice | The cursor is shared state: a field, or an int[1]. |
| 9 | Building the left subtree first in 106 | Mirrored tree, no exception thrown | Postorder is consumed from the right ⇒ right subtree first. |
| 10 | Swapping children before recursing in 226 | Double inversion; the tree comes back unchanged in some shapes | Recurse, then rewire. |
| 11 | Wrong identity for null in a DP state | Off-by-one at every leaf (968 gives a camera to each leaf) | Ask what the empty subtree contributes to each state, individually. |
| 12 | int sums on 129 / 1339 / 437 | Silent overflow on deep or wide trees | Accumulate in long; take the modulus only at the end. |
| 13 | Recomputing height inside an outer traversal (110) | O(n log n) on balanced trees, O(n²) on skewed ones | Use the -1 sentinel and one pass. |
| 14 | Assuming both targets exist in 236 | Wrong answer when one is absent | Say the assumption out loud; if unstated, return a found-flag pair. |
| 15 | One DFS for a "every node needs its own answer" problem | O(n²), TLE | That is the signature of rerooting: two passes, §2.L. |