Trees, No Gaps — Traversal · Tree Recursion · Binary Search Trees Library
Show

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:

  1. What does dfs(node) return? One number? A pair? A structure? Name the type.
  2. What is the value for null? It must be the identity element of the combining operation — 0 for a sum, 0 for a height, true for an "all nodes satisfy", Integer.MIN_VALUE for an unclamped max.
  3. 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-variantWhat dfs returnsDirectionAnswer lives in
ABottom-up aggregateone value about the subtreeupthe return value
BParallel recursion on two treesa boolean or a merged nodeupthe return value
CAugmented returnthe value the parent needsupa field/array on the side
DTop-down inherited statenothing (or a count)downan accumulator, via parameters
ERoot-to-leaf paths + backtrackingnothingdowna mutable path, undone on exit
FPrefix sums on the root pathnothingdowna map keyed by prefix, undone on exit
GLCAthe node found, or nullupthe return value
HConstruction from traversalsthe built subtreeupthe return value
ISerialization / structural identitya canonical string or hashupa map or a string
JIn-place restructuringthe new subtree rootupthe tree itself
KTree DP with per-child statesa small tuple, one entry per stateupa combination of the root's states
LRerooting (two-pass)pass 1 up, pass 2 downbothan 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#ProblemDiffSub-variantWhy it's essential
27104. Maximum Depth of Binary TreeEasyAThe atom of postorder aggregation: 1 + max(left, right), identity 0. Every Pattern 2 problem is a mutation of this shape.
28110. Balanced Binary TreeEasyAThe 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 NodesMediumAA 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.
30404. Sum of Left LeavesEasyAAggregate with a predicate that depends on the edge you arrived by, not the node. Two-minute rep.

B Parallel recursion on two trees#

Solved#ProblemDiffSub-variantWhy it's essential
31100. Same TreeEasyBTwo cursors descending in lockstep. The three-line base case (both null / one null / values differ) is the template for everything in B.
32101. Symmetric TreeEasyBSame 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.
33572. Subtree of Another TreeEasyBComposition: 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).
34617. Merge Two Binary TreesEasyBB where the return is a node instead of a boolean. Redundant if #31 is solid.
35951. Flip Equivalent Binary TreesMediumBB with a disjunction over two pairings. Good if you want one harder rep.

C Augmented return: what you record ≠ what you return#

Solved#ProblemDiffSub-variantWhy it's essential
36⚠︎543. Diameter of Binary TreeEasyCThe 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.
37124. Binary Tree Maximum Path SumHardC#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.
38687. Longest Univalue PathMediumCSame skeleton with an equality guard on each edge. Pure rep of the record/return split.
391372. Longest ZigZag Path in a Binary TreeMediumCThe 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#ProblemDiffSub-variantWhy it's essential
401448. Count Good Nodes in Binary TreeMediumDThe cleanest statement of "the parameter is the state." maxSoFar flows down; nothing flows up but a count.
41129. Sum Root to Leaf NumbersMediumDThe accumulator is transformed on the way down (cur * 10 + val) and only harvested at leaves. Watch what "leaf" means — not "null".
421315. Sum of Nodes with Even-Valued GrandparentMediumDTwo generations of inherited state instead of one. Skip if #40 was clean.

E Root-to-leaf paths with backtracking#

Solved#ProblemDiffSub-variantWhy it's essential
43112. Path SumEasyEThe 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.
44257. Binary Tree PathsEasyEFirst problem where the path is a mutable list: add before recursing, remove after. The remove is the whole sub-variant.
45113. Path Sum IIMediumE#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.
46988. Smallest String Starting From LeafMediumEBacktracking with a StringBuilder and a reversal. Good if setLength discipline is shaky.

F Prefix sums on the root path#

Solved#ProblemDiffSub-variantWhy it's essential
47437. Path Sum IIIMediumFThe 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#ProblemDiffSub-variantWhy it's essential
48236. Lowest Common Ancestor of a Binary TreeMediumGSix 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.
491123. Lowest Common Ancestor of Deepest LeavesMediumG + CLCA where the targets are not given — you must return (depth, node) upward and let the deeper side win. Sub-variants C and G composed.
501650. Lowest Common Ancestor of a Binary Tree III PROMediumGWith 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#ProblemDiffSub-variantWhy it's essential
51105. Construct Binary Tree from Preorder and Inorder TraversalMediumHPreorder 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.
52106. Construct Binary Tree from Inorder and Postorder TraversalMediumHPostorder 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.
53889. Construct Binary Tree from Preorder and Postorder TraversalMediumHShows why the answer is not unique without inorder. Worth reading even if you don't code it.
54654. Maximum Binary TreeMediumHConstruction from one array by repeatedly splitting at the max. O(n) monotonic-stack solution is a bonus.

I Serialization and structural identity#

Solved#ProblemDiffSub-variantWhy it's essential
55297. Serialize and Deserialize Binary TreeHardIThe 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.
56652. Find Duplicate SubtreesMediumICanonical serialization used as a map key. The obvious string concatenation is O(n²) characters; know the id-triple alternative.
57449. Serialize and Deserialize BSTMediumI + P3The BST version needs no null markers at all — the ordering carries the structure. Nice contrast with #55.

J In-place restructuring#

Solved#ProblemDiffSub-variantWhy it's essential
58226. Invert Binary TreeEasyJThe whole of J in three lines: recurse, then rewire. Swap after the recursive calls return, or you invert the subtrees you already swapped.
59114. Flatten Binary Tree to Linked ListMediumJTwo 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.
60117. Populating Next Right Pointers in Each Node IIMediumJ + DThe 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.
61116. Populating Next Right Pointers in Each NodeMediumJThe perfect-tree version. Strictly easier than #60 — do it only as a warm-up.
62156. Binary Tree Upside Down PROMediumJRewiring along the left spine. Free substitute: 114.

K Tree DP with per-child states#

Solved#ProblemDiffSub-variantWhy it's essential
63337. House Robber IIIMediumKThe int[]{withRoot, withoutRoot} return. The first genuinely two-dimensional state, and the model for everything else in K.
64979. Distribute Coins in Binary TreeMediumKThe 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.
65968. Binary Tree CamerasHardKThree 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.
661339. Maximum Product of Splitted Binary TreeMediumKSubtree 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#ProblemDiffSub-variantWhy it's essential
67834. Sum of Distances in TreeHardLThe 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.
68310. Minimum Height TreesMediumLTopological leaf-peeling — a different, cheaper way to exploit "every node needs an answer". Good contrast.

Extra Reps — Tree Recursion (only if a gate fails)#

SolvedProblemTargets
508. Most Frequent Subtree SumBottom-up aggregate feeding a frequency map.
1026. Maximum Difference Between Node and AncestorTop-down min/max state, sub-variant D.
863. All Nodes Distance K in Binary TreeRe-rep of the tree-as-graph transfer if §1.F failed.
1443. Minimum Time to Collect All Apples in a TreeTree DP on an adjacency list rather than a TreeNode.
2246. Longest Path With Different Adjacent CharactersSub-variant C on a general tree — the diameter argument again.
545. Boundary of Binary Tree PROThree 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.

dfs(n) sees ONLY: n.val, dfs(n.left), dfs(n.right) IDENTITY -- the null case must be the neutral element of the combine: height / sum -> 0 "all nodes satisfy" -> true unclamped max -> Integer.MIN_VALUE 110 SENTINEL ABORT: -1 means "something below is already unbalanced" short-circuits every ancestor -> O(n), not the naive O(n log n) 222 "complete" is the ALGORITHM, not decoration: equal spine heights -> perfect subtree -> 2^h - 1, no traversal at all otherwise only ONE recursive call goes deep -> O(log^2 n)

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 null without 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. 1

    What does dfs return? 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. 2

    What is the value for null? It must be the identity element of the combining operation — 0 for a sum or a height, true for a universal claim, Integer.MIN_VALUE for an unclamped max. Ask what the empty subtree contributes.

  3. 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. 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.

1 / \ 2 9 node 2's subtree is skewed: / 3 -> 4, so heights 2 and 0 differ by 2 3 / 4
#NodeleftrightCombineReturns
140 (null)0 (null)|0-0| <= 1, ok1
2310 (null)|1-0| <= 1, ok2
3220 (null)|2-0| = 2 > 1 -> unbalanced-1
41-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. -1 in LC 110 is not a hack — it is a second channel in the return, and it buys a whole complexity class. Explain the O(n) versus O(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 null

    Symptom: 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 -1 sentinel and a single pass.

  • int sums on LC 129, 1339 or 437

    Symptom: 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 null returns, returned-or-recorded.
  • Identity: the neutral element of the combine — 0, true, MIN_VALUE as appropriate.
  • LC 110: -1 sentinel gives O(n); the naive nesting gives O(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.

THE THREE-CASE BASE, and it is the whole template: if (a == b) return true // both null -- the only equal-nulls case if (a == null || b == null) return false // exactly one null if (a.val != b.val) return false // both exist, values differ then one line: recurse on the paired children 100 same tree pair (a.left, b.left) and (a.right, b.right) 101 symmetric pair (a.left, b.RIGHT) and (a.right, b.LEFT) <- CROSSED the pairing is a PARAMETER, not a different algorithm

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. 1

    Both absent. a == b catches it. Two nulls are the same reference, so this is true — structurally identical empty subtrees.

  2. 2

    Exactly one absent. If the previous test failed and either is null, the structures differ. false, immediately.

  3. 3

    Both present. Compare the values. If they differ the trees differ; if they match, the answer depends entirely on the children.

  4. 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.

1 / \ 2 2 / \ / \ 3 4 4 3
#Compared pairCaseResult
1(2, 2)both present, values equalrecurse on the crossed pairs
2(3, 3)outer-left vs outer-right, equalrecurse -> both children null
3(4, 4)inner-left vs inner-right, equalrecurse -> both children null
4(null, null)a == btrue — 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 == b is 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 == null then a == null || b == null separately

    Symptom: correct but verbose, and easy to get the order wrong.

    Prevention: a == b collapses 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: NullPointerException on 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.

543 Diameter -- THE distinction of Pattern 2 RECORD left + right the path that BENDS at this node -- uses BOTH sides, so a parent cannot extend it RETURN 1 + max(left, right) the path that CONTINUES upward -- the only thing a parent can build on 124 Maximum path sum = 543 plus one clamp RETURN node.val + max(0, max(left, right)) ^^^^^^ "a branch with a negative total is simply not taken"

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. 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. 2

    The bend uses both sides. left + right goes down one child, through the node, and down the other. Every one of the node's connections is now spent.

  3. 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. 4

    Hence record one, return the other. Record left + right into a running best; return 1 + 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.

1 / \ 2 3 / \ 4 5
#Nodeleft htright htRECORD (bend)RETURN (continue)
14000 + 0 = 01 + max(0,0) = 1
250001
32111 + 1 = 21 + max(1,1) = 2
430001
51212 + 1 = 3 <- the answer1 + 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 124

    Symptom: 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; return 1 + 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.

A bottom-up child answers -> combine -> return upward D top-down parent state -> pass down -> count 1448 goodNodes: dfs(node, maxSoFar) good = (node.val >= maxSoFar) ? 1 : 0 pass down max(maxSoFar, node.val) -- a NEW value per child return good + dfs(left, ...) + dfs(right, ...) nothing shared is mutated -> NOTHING TO UNDO that is the entire difference from sub-variants E and F

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 better
Why 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. 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. 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. 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. 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.

3 / \ 1 4 / / \ 3 1 5
#NodemaxSoFar inGood?Passes down
13 (root)3yes (3 >= 3)3
213no (1 < 3)3
333yes3
443yes (4 >= 3)4
514no4
654yes (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.val or Integer.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.

TWO RULES, both non-negotiable: 1. exactly one removeLast() for every addLast(), on EVERY exit path -- otherwise paths from one branch leak into the next 2. record a COPY -- new ArrayList<>(path) -- otherwise every result aliases the same list, which ends up empty LEAF is (left == null && right == null) NOT (n == null), which is one step PAST a leaf

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. 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. 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. 3

    A leaf is not a null. left == null && right == null is a leaf. n == null is the empty subtree one step beyond it — and recursing into both children of a leaf reaches it twice.

  4. 4

    Which is why the tests are not interchangeable. Use n == null as 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.

5 / \ 4 3 / 3 target = 8
#AtActionpath afterNote
15addLast(5)[5]enter root
24addLast(4)[5, 4]enter left
33addLast(3)[5, 4, 3]leaf, sum 12 != 8 — no record
43removeLast()[5, 4]undo, leaving the leaf
54removeLast()[5]undo, leaving node 4
63addLast(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 addLast and removeLast in 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 why n == null is 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
  • addLast without a matching removeLast

    Symptom: 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 == null as the leaf condition

    Symptom: 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 removeLast per addLast, after all recursive calls, on every exit.
  • Rule 2: record new ArrayList<>(path) — a copy, never the live list.
  • Leaf: left == null && right == null, not n == 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.

LC 560 on an array LC 437 on a tree ------------------------- ---------------------------------------- prefix sums of a[0..i] prefix sums along the CURRENT root path map: sum -> count map: sum -> count answer += map[p - k] answer += map[p - k] -- array never shrinks -- -- the path SHRINKS when you step back -- so the map must be DECREMENTED on the way up seed prefix.put(0L, 1) so a path starting at the root is counted the decrement is the LAST statement of the DFS body

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 counted
Why it works — the transplant, and the two bookkeeping rules4 steps

One idea carried across, and two details that make the carry legitimate.

  1. 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. 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 equal k.

  3. 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. 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.

5 / \ 3 3 target k = 8 / 5
#Atrunning sum plook up p - 8countmap after
1root 55map[-3] = 00{0:1, 5:1}
2left 38map[0] = 11{0:1, 5:1, 8:1}
3leaf 513map[5] = 12{0:1, 5:1, 8:1, 13:1}
4unwind to root----2{0:1, 5:1}13 and 8 removed
5right 38map[0] = 13{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} seed

    Symptom: 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.

  • int running sums

    Symptom: 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.

dfs(n) returns: a target if n IS one, otherwise whatever its children found BOTH sides non-null -> p and q are in different subtrees -> n is the SPLIT POINT -> return n ONE side non-null -> return it, unchanged. It is EITHER (a) the LCA, already found deeper, OR (b) one target that is an ancestor of the other and passing it up is correct in BOTH readings NEITHER -> return null

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. 1

    The base. null returns null; a node equal to p or q returns itself. Nothing subtle yet.

  2. 2

    Both sides non-null. One target was found somewhere left and the other somewhere right. No node below n can contain both, so n is the lowest node that does — return n.

  3. 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. 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.

3 / \ 5 1 / \ 6 2 / \ 7 4 p = 5, q = 4
#Nodeleft returnsright returnsVerdict
16nullnullreturns null
27nullnullnull
34----is a target -> returns 4
42null4one side -> pass up 4
55null (from 6)4 (from 2)is a target itself -> returns 5 immediately
635nullone 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 == q is 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) where O(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.

105 preorder + inorder preorder gives the ROOT (consume left to right) inorder says how many nodes are on its LEFT -> build LEFT subtree first 106 postorder + inorder postorder is consumed FROM THE RIGHT (root, then right, then left) -> build RIGHT subtree FIRST that single swap is the ENTIRE difference between the two the cursor is SHARED STATE: a field, or an int[1]. copying it into each frame builds subtrees from the wrong slice.

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. 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. 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. 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. 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.

preorder 3 9 20 15 7 <- consumed left to right inorder 9 3 15 20 7
#Cursor takesInorder rangeLeft sizeBuilds
13[9, 3, 15, 20, 7]1 (just 9)root 3; recurse left on [9]
29[9]0leaf 9; both sides empty
320[15, 20, 7]1 (just 15)node 20 — the cursor is now at index 2, which only works because frame 2 advanced it
415[15]0leaf 15
57[7]0leaf 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) versus O(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 pre cursor into each frame

    Symptom: 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 + null markers UNIQUELY DECODABLE 3 , 9 , # , # , 20 , 15 , # , # , 7 , # , # the first token is always the root of the next subtree preorder ALONE ambiguous -- no way to know where a subtree ends inorder + markers STILL fails: the root is somewhere in the MIDDLE and nothing marks which token it is deserialize uses ONE SHARED CURSOR, exactly as in sub-variant H

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. 1

    Preorder alone is ambiguous. 3, 9, 20 could be a left chain, a right chain, or a root with two children. Nothing says where a subtree ends.

  2. 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. 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. 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.

1 / \ 2 3 / 4
#Encode stepEmitsDecode stepBuilds
1visit 11read 1root 1, then decode its left
2visit 2, both children null2, #, #read 2, #, #leaf 2 — both # end it
3visit 3, left is 43read 3node 3; the next token belongs to its left
4visit 4, both null4, #, #read 4, #, #leaf 4
53'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,2 and 12 become 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.

226 INVERT: recurse, THEN swap. swapping first inverts subtrees twice and the tree can come back unchanged 114 FLATTEN, O(1) space: for each node, splice the LEFT subtree between the node and its right subtree; the RIGHTMOST node of the left subtree inherits the old right chain n n / \ -> \ L R L ... -> R 117 O(1)-space level order: the level you have ALREADY LINKED is the queue for the next one a DUMMY HEAD removes every "is this the first child on this level?" case

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. 1

    Mutation invalidates what the children saw. If you swap left and right before recursing, the recursive calls operate on subtrees that have already moved — and invert them a second time.

  2. 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. 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. 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.

1 / \ 2 5 / \ \ 3 4 6
#AtLeft subtreeRightmost of itRewire
1node 12 -> (3, 4)44.right = 5; 1.right = 2; 1.left = null
2node 2333.right = 4; 2.right = 3; 2.left = null
3node 3none--move on to 3.right
4node 4none--move on
5nodes 5, 6none--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 right is 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 left after splicing

    Symptom: the result is not a right-only chain, so it fails the problem's definition.

    Prevention: node.left = null as 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.

337 ROB: return {rob this node, do NOT rob this node} null -> identity for BOTH states -- an empty subtree contributes nothing either way answer: max(rob(root)[0], rob(root)[1]) 979 COINS: the return value is a SURPLUS and may be NEGATIVE. you count FLOW ACROSS AN EDGE, not nodes: |surplus| coins must cross the edge to the parent, in either direction 968 CAMERAS: three states, greedy postorder 0 = uncovered 1 = covered, no camera here 2 = has a camera place a camera as LATE as possible -- at the PARENT of an uncovered node, because a camera at the parent covers strictly more null MUST be 1: an absent child needs no cover, otherwise every leaf gets a camera answer: cameras + (cover(root) == 0 ? 1 : 0) -- the root can end up uncovered

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 uncovered
Why 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. 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. 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. 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 null must be state 1 — return 0 instead and every leaf gets a camera it does not need.

  4. 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.

3 / \ 2 3 \ \ 3 1
#Nodechildren returnrob = val + sum(child skips)skip = sum(max of child)
1leaf 3--30
2leaf 1--10
32{3, 0}2 + 0 = 2max(3,0) = 3
4right 3{1, 0}3 + 0 = 3max(1,0) = 1
5root 3{2,3} and {3,1}3 + 3 + 1 = 7max(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 null in a DP state

    Symptom: 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 null value per state.
  • Return: a small tuple, one entry per state — never a single collapsed best.
  • LC 968: place cameras as late as possible; null is covered; check the root at the end.
  • Gate: design LC 968's three states, transition and null value 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.

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 n - size[c] nodes 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

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. 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. 2

    Pass 1 collects what is local. A postorder walk gives size[v] for every node, and accumulates ans[root], the sum of distances from the root.

  3. 3

    Pass 2 moves the root by one edge. Going from parent p to child c: every node inside c's subtree is now one step closer, and every node outside it is one step further away.

  4. 4

    Which is the identity. ans[c] = ans[p] - size[c] + (n - size[c]), and simplifying gives ans[c] = ans[p] + n - 2 * size[c]. A preorder pass propagates it to the whole tree in O(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.

0 / \ 1 2 /|\ 3 4 5 n = 6, ans[0] = 8
#Move rootsize[c]closerfurtherans[c]
10 -> 111 node5 nodes8 + 6 - 2(1) = 12
20 -> 244 nodes2 nodes8 + 6 - 2(4) = 6
32 -> 311 node5 nodes6 + 6 - 2(1) = 10
42 -> 411 node5 nodes10
52 -> 511 node5 nodes10

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 size includes the node itself

    Symptom: off-by-one in every delta.

    Prevention: size[v] counts v plus all its descendants.

Key takeaway

  • Trigger: every node needs its own answer, each depending on the whole tree.
  • Two passes: postorder for size[] and ans[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#

#BugSymptomPrevention
1Returning the recorded value in 543 / 124Plausible but wrong on any tree where the best path bends below the rootSay it out loud before coding: record the bend, return the continuation.
2Omitting Math.max(0, child) in 124Wrong on all-negative treesA branch with a negative total is never taken. The clamp encodes that.
3Treating n == null as the leaf conditionRoot-to-leaf answers counted twice, or half-paths acceptedA leaf is left == null && right == null. Both tests exist and mean different things.
4addLast without a matching removeLastPaths from one branch leak into the nextOne undo per mutation, placed after all recursive calls, on every exit path.
5Recording the path list instead of a copyEvery result is the same (empty) listnew ArrayList<>(path) at the moment of recording.
6Prefix map not decremented on the way upOvercounts paths that span two branchesThe decrement is the last statement of the DFS body.
7Prefix map missing the {0: 1} seedMisses every path that starts at the rootSeed before the first call, not inside it.
8Copying the pre cursor into each frame in 105Subtrees built from the wrong sliceThe cursor is shared state: a field, or an int[1].
9Building the left subtree first in 106Mirrored tree, no exception thrownPostorder is consumed from the right ⇒ right subtree first.
10Swapping children before recursing in 226Double inversion; the tree comes back unchanged in some shapesRecurse, then rewire.
11Wrong identity for null in a DP stateOff-by-one at every leaf (968 gives a camera to each leaf)Ask what the empty subtree contributes to each state, individually.
12int sums on 129 / 1339 / 437Silent overflow on deep or wide treesAccumulate in long; take the modulus only at the end.
13Recomputing height inside an outer traversal (110)O(n log n) on balanced trees, O(n²) on skewed onesUse the -1 sentinel and one pass.
14Assuming both targets exist in 236Wrong answer when one is absentSay the assumption out loud; if unstated, return a found-flag pair.
15One DFS for a "every node needs its own answer" problemO(n²), TLEThat is the signature of rerooting: two passes, §2.L.