The finale. First you exploit structure, counting a complete tree's nodes in O(log²n) and flattening a tree into a list in O(1) space. Then you run traversals backwards: reconstruct the exact tree from its preorder+inorder (or inorder+postorder), and serialize it to a string and back. You'll meet Morris traversal, which walks a tree in O(1) space by temporarily threading it. And you'll close on tree DP (House Robber III and Binary Tree Cameras) where each node returns a small tuple of sub-answers and the whole tree folds up in one postorder.
This is not a list of problems. It is 8 learning units, and every one runs the same four beats. Go in order and the problems stop feeling random.
7 PROBLEMS · EVERY ONE LINKS TO LEETCODE TO SOLVE
Click any problem to jump straight to it. Solved ones turn green. Press I from anywhere to come back here. The number is the sheet's row, not your position — the deck teaches in the order that builds, so you will meet them slightly out of numerical sequence.
The finale's toolbox. The cards are the phrase that selects it: “complete tree” → the log² shortcut, “from traversals” → reconstruct with an index map, “O(1) space” → Morris/in-place threading, “no two adjacent” → tree DP.
left-height == right-height means a perfect subtree of 2^h − 1 nodes, no walk
COMPLETE-TREE SHORTCUTO(log²n)reuse the tree's own right pointers; thread the left subtree in before the right
IN-PLACE REWIREO(n) · O(1) spacepre/post gives the root, inorder splits left|right; recurse with an index map
RECONSTRUCT FROM SEQUENCESO(n)serialize with null markers (preorder or BFS); deserialize by consuming tokens
SERIALIZE + DESERIALIZEO(n)temporarily thread each node to its inorder predecessor, then undo the thread
MORRIS TRAVERSALO(n) · O(1) spaceeach node returns a small tuple of sub-answers; combine in one postorder
TREE DPO(n)Most of this deck is one O(n) pass. The two special cases: counting a complete tree beats O(n) with the equal-heights shortcut (O(log²n)), and Morris/flatten reach O(1) extra space by threading the tree instead of using a stack.
TREE FINALE ⇒ O(n) PASS · COMPLETE-TREE ⇒ O(log²n) · THREADING ⇒ O(1) SPACE
Eight units. Structure exploits (count complete, flatten) open; then reconstruction from pre+in and in+post, serialize/deserialize, Morris (a concept unit, O(1)-space traversal), and the two canonical tree DPs. This closes Binary Trees; BST (Step 14) is next.
To reconstruct a unique binary tree, which pair of traversals is enough, and which pair is NOT?
Inorder is the essential ingredient. Preorder or postorder tells you the root; inorder then tells you which nodes fall in the left subtree vs the right (everything before the root vs after). Without inorder, e.g. preorder+postorder, a node with a single child is ambiguous (left or right?), so the tree isn't unique. This is the 'requirements for a unique tree' theory (L33), folded into this deck.
Tree DP (House Robber III, Cameras) has each node return information to its parent. Why a tuple/pair rather than a single number?
The parent needs several conditional sub-answers. In House Robber III a node must know both 'best if this child is robbed' and 'best if it isn't', because robbing the parent forbids robbing the child. A single scalar loses that; a pair (rob, notRob) carries exactly what the parent needs to combine. Tree DP is 'return a small tuple, fold up in one postorder'.
Counting the nodes of an arbitrary tree is a trivial O(n) walk, but a complete tree lets you do it in O(log²n). At any node, measure the left height (keep going left) and the right height (keep going right). If they're equal, the subtree is perfect and has exactly 2ʰ − 1 nodes. Return that without touching them. Only when the heights differ (the one ragged path) do you recurse into both children.
HOW DO YOU COUNT A COMPLETE TREE'S NODES WITHOUT VISITING THEM ALL?
Why does equal left-height and right-height mean the subtree is perfect?
Same spine lengths ⇒ no gaps ⇒ every level full. A complete tree fills left-to-right, so a shortfall would show up as the rightmost path being shorter than the leftmost. When they match, the last level is full too, making it perfect, 2ʰ − 1 nodes by formula, computed without a single node-by-node visit.
Where does the O(log²n) come from?
O(log n) recursions × O(log n) height check. Every perfect subtree is dispatched by formula in O(log n) (the height measurement) with no recursion. Only the one path where left and right heights disagree recurses, and that path has length O(log n). Multiply and you get O(log²n), dramatically better than the naive O(n).
Worth seeing precisely because it is dull. Identical walk, identical dependency, and the combine line is just 1 + L + R. Once you can see that depth, balance, diameter, max-path-sum and node-count are one function with one line swapped, deck 2 stops being sixteen problems and becomes about three. (For a complete tree there is a genuinely faster O(log²n) method that compares left and right spines, but the linear version is the one to reach for first, and the one every sibling problem is built on.)
“Count nodes of a COMPLETE binary tree, better than O(n).” 'Complete' is the signal to exploit the shape.
Measure left height (all-left) and right height (all-right). If equal, the subtree is perfect: return 2^h − 1. Otherwise recurse into both children and add 1.
// Perfect subtrees counted by formula; recurse only the ragged path. int leftH(TreeNode* n) { int h = 0; while (n) { h++; n = n->left; } return h; } int rightH(TreeNode* n) { int h = 0; while (n) { h++; n = n->right; } return h; } int countNodes(TreeNode* root) { if (!root) return 0; int lh = leftH(root), rh = rightH(root); if (lh == rh) return (1 << lh) - 1; // perfect: 2^lh - 1 return 1 + countNodes(root->left) + countNodes(root->right); }
// Perfect subtrees counted by formula; recurse only the ragged path. int leftH(TreeNode n) { int h = 0; while (n != null) { h++; n = n.left; } return h; } int rightH(TreeNode n) { int h = 0; while (n != null) { h++; n = n.right; } return h; } public int countNodes(TreeNode root) { if (root == null) return 0; int lh = leftH(root), rh = rightH(root); if (lh == rh) return (1 << lh) - 1; // perfect: 2^lh - 1 return 1 + countNodes(root.left) + countNodes(root.right); }
# Perfect subtrees counted by formula; recurse only the ragged path. def countNodes(root): def leftH(n): h = 0 while n: h, n = h + 1, n.left return h def rightH(n): h = 0 while n: h, n = h + 1, n.right return h if not root: return 0 lh, rh = leftH(root), rightH(root) if lh == rh: return (1 << lh) - 1 # perfect: 2^lh - 1 return 1 + countNodes(root.left) + countNodes(root.right)
Falling back to an O(n) walk. Counting every node ignores the 'complete' guarantee. The equal-heights shortcut dispatches whole perfect subtrees by formula, so only the single ragged path recurses, O(log²n). Measure heights with a plain left/right spine walk, not a full traversal.
You can rebuild the exact tree from its preorder and inorder sequences. Preorder's first element is the root. Find that root in inorder: everything to its left is the left subtree, everything to its right is the right subtree. Recurse, advancing a preorder index. A value → inorder index hashmap makes the split O(1), keeping the whole build O(n). (Inorder is the essential half. It's what separates left from right.)
HOW DO YOU RECONSTRUCT THE UNIQUE TREE FROM PREORDER + INORDER?
What role does each of the two traversals play in the reconstruction?
Preorder → root; inorder → split. Preorder visits root-first, so its front is always the next root. Once you know the root's value, its index in inorder cleaves the remaining nodes into 'before' (left subtree) and 'after' (right subtree). Recursing with those bounds and the advancing preorder index rebuilds the tree uniquely.
Why build a hashmap of inorder values to indices instead of scanning inorder each time?
O(1) root location keeps the build O(n). The recursion visits each node once, but finding the root's split point by scanning is O(n) each time. Quadratic. Precomputing value → inorder index once turns every split into a constant-time lookup. Assumes distinct values, which these problems guarantee.
Preorder is root → left → right: you record a node the moment you reach it, before going into either subtree. The recursion is three lines. Record, go left, go right, and the call stack on the right is nothing more than the path from the root down to wherever you currently are. Watch the badges land: the root is always #1, and a subtree is numbered completely before its sibling gets started. This is the order you would copy or serialize a tree in, because the parent always comes out ahead of its children.
“Reconstruct the tree from preorder + inorder.” Two sequences, one unique tree.
Preorder's front is the root. Locate it in inorder (via a value→index map) to split left and right subtrees; recurse, advancing the preorder index.
// Preorder gives roots; inorder (via a map) splits each subtree. unordered_map<int,int> pos; int preIdx = 0; TreeNode* build(vector<int>& pre, int inL, int inR) { if (inL > inR) return nullptr; int rootVal = pre[preIdx++]; // next root, preorder front-to-back TreeNode* root = new TreeNode(rootVal); int mid = pos[rootVal]; // its split point in inorder root->left = build(pre, inL, mid - 1); root->right = build(pre, mid + 1, inR); return root; } TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) { for (int i = 0; i < inorder.size(); i++) pos[inorder[i]] = i; preIdx = 0; return build(preorder, 0, inorder.size() - 1); }
// Preorder gives roots; inorder (via a map) splits each subtree. Map<Integer,Integer> pos = new HashMap<>(); int preIdx = 0; TreeNode build(int[] pre, int inL, int inR) { if (inL > inR) return null; int rootVal = pre[preIdx++]; // next root, preorder front-to-back TreeNode root = new TreeNode(rootVal); int mid = pos.get(rootVal); // its split point in inorder root.left = build(pre, inL, mid - 1); root.right = build(pre, mid + 1, inR); return root; } public TreeNode buildTree(int[] pre, int[] in) { for (int i = 0; i < in.length; i++) pos.put(in[i], i); return build(pre, 0, in.length - 1); }
# Preorder gives roots; inorder (via a map) splits each subtree. def buildTree(preorder, inorder): pos = {v: i for i, v in enumerate(inorder)} preIdx = 0 def build(inL, inR): nonlocal preIdx if inL > inR: return None rootVal = preorder[preIdx]; preIdx += 1 # next root, front-to-back root = TreeNode(rootVal) mid = pos[rootVal] # split point in inorder root.left = build(inL, mid - 1) root.right = build(mid + 1, inR) return root return build(0, len(inorder) - 1)
Scanning inorder for the root each call. O(n²). Precompute a value→index map so the split is O(1). Also build LEFT before RIGHT here (preorder is root-left-right), and advance the shared preorder index exactly once per node.
Inorder + postorder rebuilds the tree the same way, with two twists. Postorder's LAST element is the root (postorder ends with the root), so you consume postorder from the back. And because you're taking roots in reverse (root, then right, then left), you must build the right subtree before the left. Same inorder split, same index hashmap.
HOW DOES CONSTRUCTION CHANGE FOR INORDER + POSTORDER?
Why must you build the right subtree before the left when using postorder from the back?
Backwards postorder is root-right-left. Postorder is left-right-root; reversed it's root-right-left. As you pop roots from the back, the immediately following ones are the right subtree's nodes, so you must recurse right before left to keep the index aligned. Building left-first (as in pre+in) would grab the wrong nodes.
What is common to both construction problems?
Inorder split + index map is the shared engine. Whichever of preorder/postorder you pair with inorder, inorder does the left|right partition and the hashmap keeps it O(n). The only differences are where the root comes from (front of preorder vs back of postorder) and, consequently, the recursion order. Recognising the shared skeleton is the point.
Neither traversal can rebuild a tree alone. preorder tells you the roots in order but not where each subtree ends; inorder tells you where a root splits its range but not which node is the root. Together they are exact. Take the next value from preorder, find it in inorder, and everything to its left in that array is its left subtree, everything to the right is its right. Building from inorder + postorder is the identical method with one change: postorder gives the roots from the end backwards, so you consume it in reverse and build the right subtree first. The scan for the root is what makes a naive version O(n²). A value-to-index map takes it to O(n).
“Reconstruct the tree from inorder + postorder.” Same idea, postorder from the back.
Postorder's last element is the root. Consume postorder from the back; because roots come out root-right-left, build the RIGHT subtree before the left. Inorder splits as before.
// Postorder (from the back) gives roots; build RIGHT before left. unordered_map<int,int> pos; int postIdx; TreeNode* build(vector<int>& post, int inL, int inR) { if (inL > inR) return nullptr; int rootVal = post[postIdx--]; // roots come out root-right-left TreeNode* root = new TreeNode(rootVal); int mid = pos[rootVal]; root->right = build(post, mid + 1, inR); // RIGHT first root->left = build(post, inL, mid - 1); return root; } TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) { for (int i = 0; i < inorder.size(); i++) pos[inorder[i]] = i; postIdx = postorder.size() - 1; return build(postorder, 0, inorder.size() - 1); }
// Postorder (from the back) gives roots; build RIGHT before left. Map<Integer,Integer> pos = new HashMap<>(); int postIdx; TreeNode build(int[] post, int inL, int inR) { if (inL > inR) return null; int rootVal = post[postIdx--]; // roots come out root-right-left TreeNode root = new TreeNode(rootVal); int mid = pos.get(rootVal); root.right = build(post, mid + 1, inR); // RIGHT first root.left = build(post, inL, mid - 1); return root; } public TreeNode buildTree(int[] in, int[] post) { for (int i = 0; i < in.length; i++) pos.put(in[i], i); postIdx = post.length - 1; return build(post, 0, in.length - 1); }
# Postorder (from the back) gives roots; build RIGHT before left. def buildTree(inorder, postorder): pos = {v: i for i, v in enumerate(inorder)} postIdx = len(postorder) - 1 def build(inL, inR): nonlocal postIdx if inL > inR: return None rootVal = postorder[postIdx]; postIdx -= 1 # root-right-left order root = TreeNode(rootVal) mid = pos[rootVal] root.right = build(mid + 1, inR) # RIGHT first root.left = build(inL, mid - 1) return root return build(0, len(inorder) - 1)
Building left before right. Consuming postorder from the back yields roots in root-right-left order, so you must recurse RIGHT first to align the index. Doing it left-first (the pre+in habit) grabs the wrong nodes and produces a mirrored/scrambled tree. Same inorder map, opposite recursion order.
Serialize turns a tree into a string so it can be stored or sent; deserialize rebuilds the exact tree from that string. The key is explicit null markers. Do a preorder walk, emitting each value (and a sentinel like # for every null child) separated by commas. To rebuild, consume the tokens in the same preorder: a value creates a node and recurses for its two children; a # returns null. The markers make the shape unambiguous.
HOW DO YOU FLATTEN A TREE TO A STRING AND REBUILD IT EXACTLY?
Why are null markers essential in the serialized string?
Markers disambiguate the shape. The values alone (say 1,2,3) fit many trees; you can't tell whether 2 is a left child with no children or has a subtree. Emitting # for every absent child records the structure precisely, so the token stream reconstructs a unique tree. BFS with markers works identically.
How does deserialize know when to stop building a subtree?
The token order drives the recursion. Reading tokens front-to-back in the same preorder used to write them, a # immediately caps a branch (return null) and a value opens a node whose children are the next tokens. No counting or lengths needed. The interleaved values and markers are the structure.
Writing the values out level by level is not enough: many different trees flatten to the same list of values, so the string could not be read back. What makes the encoding reversible is writing a marker for every missing child. Those markers are the structure. Deserialising is then the same loop run backwards. Read a value, and the next two entries are its children, marker or not. It is the cleanest illustration in the deck that a traversal alone loses shape, which is also why preorder alone cannot rebuild a tree but preorder with null markers can.
“Serialize a tree to a string and deserialize it back.” Encode with null markers.
Serialize by preorder, emitting each value and a '#' for every null, comma-separated. Deserialize by consuming those tokens in order: a value makes a node and recurses for two children, a '#' returns null.
// Preorder with '#' null markers; deserialize consumes tokens in order. string serialize(TreeNode* root) { string s; function<void(TreeNode*)> dfs = [&](TreeNode* n) { if (!n) { s += "#,"; return; } s += to_string(n->val) + ","; dfs(n->left); dfs(n->right); }; dfs(root); return s; } TreeNode* deserialize(string data) { int i = 0; function<TreeNode*()> build = [&]() -> TreeNode* { int j = data.find(',', i); string tok = data.substr(i, j - i); i = j + 1; if (tok == "#") return nullptr; TreeNode* n = new TreeNode(stoi(tok)); n->left = build(); n->right = build(); return n; }; return build(); }
// Preorder with '#' null markers; deserialize consumes tokens in order. public String serialize(TreeNode root) { StringBuilder sb = new StringBuilder(); dfs(root, sb); return sb.toString(); } private void dfs(TreeNode n, StringBuilder sb) { if (n == null) { sb.append("#,"); return; } sb.append(n.val).append(','); dfs(n.left, sb); dfs(n.right, sb); } private int idx; public TreeNode deserialize(String data) { idx = 0; return build(data.split(",")); } private TreeNode build(String[] t) { String tok = t[idx++]; if (tok.equals("#")) return null; // marker ends this branch TreeNode node = new TreeNode(Integer.parseInt(tok)); node.left = build(t); // stream order encodes the shape node.right = build(t); return node; }
# Preorder with '#' null markers; deserialize consumes tokens in order. class Codec: def serialize(self, root): out = [] def dfs(n): if not n: out.append('#'); return out.append(str(n.val)) dfs(n.left); dfs(n.right) dfs(root) return ','.join(out) def deserialize(self, data): it = iter(data.split(',')) def build(): tok = next(it) if tok == '#': return None n = TreeNode(int(tok)) n.left = build(); n.right = build() return n return build()
Omitting null markers. Without a sentinel for every absent child the string is ambiguous and can't be rebuilt uniquely. Emit '#' (or 'null') for each null. Deserialize must consume tokens in the exact order they were written. A shared index/iterator does this naturally.
Morris traversal walks a tree in O(1) extra space, no stack, no recursion, by temporarily threading each node to its inorder predecessor. For inorder: if a node has a left child, find the rightmost node of that left subtree (the predecessor); point its right pointer back to the current node (the thread), then go left. When you return via that thread, you remove it (restoring the tree), record the node, and go right. Elegant, and a concept unit. The technique matters, not a judge row.
HOW DO YOU DO INORDER IN O(1) SPACE WITH NO STACK?
What does Morris traversal use instead of a stack to get back up the tree?
A temporary right-pointer thread to the predecessor. Normally you'd need a stack to return to a node after its left subtree. Morris instead points the left subtree's rightmost node's (otherwise null) right pointer back at the current node, so the traversal walks up that link for free. O(1) space. The cost is a second visit to each threaded node (to undo the link).
Why must the thread be removed on the second visit?
Leaving the thread creates a cycle and corrupts the tree. On reaching a node whose predecessor already threads back to it, you know the left subtree is finished: remove the thread (restore the null), record the node (for inorder), and move right. Skipping the removal loops forever and mutates the input. the whole discipline of Morris is 'thread, then always unthread'.
Inorder is left → root → right: a node is not recorded until its entire left subtree is done. One thing moved. The record line now sits between the two calls instead of before them, and the whole visit order changes. Watch which node gets badge #1: the leftmost leaf, not the root. On a binary search tree this order emits the values sorted, and that single fact is why inorder turns up on nearly every problem in the BST section.
Flatten rearranges the tree into a right-skewed linked list that follows preorder (each node's left becomes null, its right points to the next preorder node). The O(1)-space trick is a Morris-like rewire: for each current node, if it has a left subtree, find that subtree's rightmost node, attach the current node's right subtree there, move the left subtree to the right, null the left, and advance. The tree's own pointers become the list.
HOW DO YOU FLATTEN A TREE INTO A PREORDER LIST IN O(1) SPACE?
In the O(1) flatten, why attach the current node's right subtree to the rightmost node of its left subtree?
The right subtree continues after the left subtree ends. Flattened order is node, then everything on the left, then everything on the right. So you move the left subtree onto the node's right, and the previous right subtree must hang off the very end of that moved-in left subtree, its rightmost node. Then advance and repeat; each node's left ends up null and its right is the next preorder node.
What makes this flatten O(1) space rather than O(n) (e.g. collecting preorder into a list)?
In-place pointer surgery, no auxiliary storage. The naive approach records preorder into an array then relinks, O(n) space. The Morris-like method reuses the tree's own left/right fields, threading the right subtree onto the left subtree's tail and shifting left to right. Only a handful of pointer locals are needed, so it's O(1) extra space.
Flatten rewrites the tree in place into a right-leaning list, and the order it lands in is preorder. The obvious approach. Walk preorder and re-point as you go, destroys the pointer to the right subtree before you have visited it. The fix is to work in reverse: recurse right, then left, then attach the current node in front of the already-finished tail, kept in a single prev variable. Every node then points at something already correct, so nothing is overwritten before it has been used. Reverse-postorder is exactly preorder backwards, which is why this produces the sequence it does.
“Flatten the tree to a right-skewed linked list, preorder order, in place.” O(1) space via a Morris-like rewire.
For each node with a left subtree, find that subtree's rightmost node, attach the node's current right subtree there, move the left subtree to the right, null the left, and advance to the right. The tree's own pointers become the list.
// In-place: splice the right subtree onto the left subtree's tail. void flatten(TreeNode* root) { TreeNode* cur = root; while (cur) { if (cur->left) { TreeNode* prev = cur->left; while (prev->right) prev = prev->right; // rightmost of left subtree prev->right = cur->right; // old right hangs off it cur->right = cur->left; // left subtree becomes right cur->left = nullptr; } cur = cur->right; // advance down the list } }
// In-place: splice the right subtree onto the left subtree's tail. public void flatten(TreeNode root) { TreeNode cur = root; while (cur != null) { if (cur.left != null) { TreeNode prev = cur.left; while (prev.right != null) prev = prev.right; // rightmost of left prev.right = cur.right; // old right hangs off it cur.right = cur.left; // left subtree becomes right cur.left = null; } cur = cur.right; // walk the growing list } }
# In-place: splice the right subtree onto the left subtree's tail. def flatten(root): cur = root while cur: if cur.left: prev = cur.left while prev.right: # rightmost of left subtree prev = prev.right prev.right = cur.right # old right hangs off it cur.right = cur.left # left subtree becomes right cur.left = None cur = cur.right # advance down the list
Collecting preorder into a list first. O(n) space. That works but misses the point. The in-place rewire threads the right subtree onto the rightmost node of the left, moves left to right, and nulls the left, reusing the tree's own pointers for O(1) extra space. Don't forget to null the left pointer, or the list still branches.
House Robber III is the canonical tree DP. You may not rob two directly connected houses (a node and its child). Each node returns a pair: (rob, notRob). If you rob this node you take its value plus the not-robbed results of both children (they must be skipped): rob = val + notRob(L) + notRob(R). If you don't, each child is free to be robbed or not: notRob = max(rob,notRob)(L) + max(rob,notRob)(R). The answer is the max of the root's pair.
HOW DO YOU MAXIMISE A SUM WITH 'NO TWO ADJACENT' ON A TREE?
Why must each node return BOTH the rob and not-rob values, not just the better one?
The parent needs the child's not-robbed value. Robbing the parent forbids robbing its children, so the parent must add the children's not-robbed totals, a value that would be lost if the child collapsed to max(rob, notRob). Returning the full pair preserves exactly the information a parent might need either way; the max is taken only once, at the root.
What is notRob for a node, in terms of its children?
Children are free when the parent is skipped. Not robbing the current node places no restriction on its children, so each contributes its own maximum. max(rob, notRob). Independently. Summing those gives the node's not-robbed value. Contrast rob, which forces the children's not-robbed values. Two clean cases, folded in one postorder.
Every earlier member of the postorder-value family returned a single number. This one cannot, and the reason is worth the slide: robbing a node forbids robbing either child, so the parent needs each child's “best if I was skipped” figure, not merely its best. Return both. (robbed, skipped), and the recursion stays one clean pass. Collapse it to one number too early and you get a greedy answer that is wrong on trees where taking a child beats taking the parent. This is the tree version of the classic take-or-skip DP, and the shape carries directly to Cameras.
“Max sum with no two directly-connected houses robbed.” A tree DP returning a pair.
Each node returns (rob, notRob). Rob = val + notRob of both children. NotRob = sum of each child's max(rob, notRob). Answer is the root's max.
// Return (rob, notRob); collapse to max only at the root. pair<int,int> solve(TreeNode* node) { if (!node) return {0, 0}; auto [lr, ln] = solve(node->left); auto [rr, rn] = solve(node->right); int rob = node->val + ln + rn; // rob node -> skip children int notRob = max(lr, ln) + max(rr, rn); // skip node -> children free return {rob, notRob}; } int rob(TreeNode* root) { auto [r, nr] = solve(root); return max(r, nr); }
// Return (rob, notRob) as a 2-slot array; collapse to max only at the root. int[] solve(TreeNode node) { if (node == null) return new int[]{0, 0}; int[] l = solve(node.left); int[] r = solve(node.right); int rob = node.val + l[1] + r[1]; // rob -> skip children int notRob = Math.max(l[0], l[1]) + Math.max(r[0], r[1]); // children free return new int[]{rob, notRob}; } public int rob(TreeNode root) { int[] a = solve(root); return Math.max(a[0], a[1]); }
# Return (rob, notRob); collapse to max only at the root. def rob(root): def solve(node): if not node: return (0, 0) lr, ln = solve(node.left) rr, rn = solve(node.right) do_rob = node.val + ln + rn # rob node -> skip children skip = max(lr, ln) + max(rr, rn) # skip node -> children free return (do_rob, skip) return max(solve(root))
Returning a single max up the tree. The parent needs the child's not-robbed value specifically (robbing the parent forbids robbing the child), which is lost if you collapse to max early. Return the full (rob, notRob) pair and take the max only at the root.
Binary Tree Cameras places the fewest cameras so every node is monitored (a camera covers its own node, its parent, and its children). It's a greedy postorder with three states: 0 = uncovered, 1 = covered (a child has a camera), 2 = has a camera. A null returns covered (1), so leaves come back uncovered (0). Going bottom-up, if either child is uncovered you must place a camera here; if a child has a camera you're covered; otherwise you're uncovered and defer to your parent. Place cameras as high as possible.
HOW DO YOU COVER EVERY NODE WITH THE FEWEST CAMERAS?
Why does a null node return 'covered' (1), and what does that achieve for leaves?
Null-as-covered pushes cameras up to parents of leaves. If nulls were uncovered, you'd camera every leaf. Wasteful. Returning covered for null makes leaves uncovered, so their parents (one level up) take the camera, each covering three levels' worth of nodes. Placing cameras at the parents of uncovered nodes, as high as possible, is the greedy optimum.
After the postorder returns, why might you still need to add one camera at the root?
The root has no parent to defer to. A node in state 0 normally relies on its parent to place the covering camera. The root has none, so if the recursion returns 0 for the root, it's still uncovered. You add a final camera there. Every other uncovered node was already handled by its parent during the bottom-up pass.
Cameras is the pair idea widened to three states. Needs cover, has a camera, covered, and a genuinely greedy decision: place a camera only when a child is still uncovered, and place it on the parent. That choice is the whole optimisation. A camera on a leaf covers the leaf and its parent; the same camera one level up covers the leaf, its sibling and the parent. Strictly better, always. So leaves should never hold one. The one case the recursion cannot handle is the root: it has no parent to fall back on, so if it comes back still uncovered it must buy its own.
“Fewest cameras so every node is monitored.” A 3-state greedy postorder.
States: 0 uncovered, 1 covered, 2 has a camera. Null returns 1. If either child is uncovered (0), place a camera here (return 2); if a child has a camera (2), you're covered (1); else you're uncovered (0). Add one at the root if it ends uncovered.
// 3-state greedy: camera only when a child is uncovered. int cameras = 0; int dfs(TreeNode* node) { // 0 uncovered, 1 covered, 2 has camera if (!node) return 1; // null counts as covered int l = dfs(node->left), r = dfs(node->right); if (l == 0 || r == 0) { cameras++; return 2; } // must cover a child if (l == 2 || r == 2) return 1; // a child's camera covers us return 0; // uncovered -> parent handles } int minCameraCover(TreeNode* root) { cameras = 0; if (dfs(root) == 0) cameras++; // root left uncovered return cameras; }
// 3-state greedy: camera only when a child is uncovered. int cameras = 0; int dfs(TreeNode node) { // 0 uncovered, 1 covered, 2 has camera if (node == null) return 1; // null counts as covered int l = dfs(node.left), r = dfs(node.right); if (l == 0 || r == 0) { cameras++; return 2; } // must cover a child if (l == 2 || r == 2) return 1; // a child's camera covers us return 0; // uncovered: parent's job } public int minCameraCover(TreeNode root) { return (dfs(root) == 0 ? 1 : 0) + cameras; // root uncovered? one more }
# 3-state greedy: camera only when a child is uncovered. def minCameraCover(root): cameras = 0 def dfs(node): # 0 uncovered, 1 covered, 2 has camera nonlocal cameras if not node: return 1 # null counts as covered l, r = dfs(node.left), dfs(node.right) if l == 0 or r == 0: cameras += 1; return 2 # must cover an uncovered child if l == 2 or r == 2: return 1 # a child's camera covers us return 0 # uncovered -> parent handles it if dfs(root) == 0: cameras += 1 # root left uncovered return cameras
Cameraing leaves, or forgetting the root fix-up. Null-as-covered makes leaves uncovered so their parents take the camera (covering three levels), never camera a leaf. And since the root has no parent, if the recursion returns 0 for it, add one final camera. Greedy 'cameras as high as possible' is provably optimal here.
Counting nodes in a COMPLETE binary tree in O(log²n): what shortcut avoids visiting every node?
Equal left/right heights ⇒ a perfect subtree, counted by formula. A complete tree is perfect except possibly the last level. If the leftmost and rightmost spines are the same length, every level is full: 2ʰ − 1 nodes, no traversal. Only along the one 'ragged' path do heights differ and you recurse, giving O(log n) recursions each doing an O(log n) height check.
Binary Tree Cameras uses a greedy postorder with three node states. What are they, and when do you place a camera?
Place a camera only when a child is uncovered. Nulls are treated as covered (return 1), so leaves are uncovered (0). You never waste a camera on a leaf; instead the leaf's parent gets one, covering the leaf, the parent and the grandparent. Greedily placing cameras as high as possible (at parents of uncovered nodes) is optimal. The three states encode exactly the information the parent needs.
Finale bugs are quiet: an O(n²) construction from a linear root search, left-before-right in the postorder build, a forgotten Morris un-thread, missing null markers, a tree-DP tuple collapsed too early. Each returns a believable answer.
Scanning inorder to find the root's split point every call is O(n) per node. Build a value → inorder index hashmap once so the split is O(1), keeping the whole build O(n).
Consuming postorder from the back gives roots in root-right-left order, so you must build the right subtree before the left. Do it left-first (as in the pre+in version) and the tree comes out wrong.
Just counting every node is O(n) and misses the point. Use the equal-heights shortcut to skip whole perfect subtrees; only the single ragged path costs recursion, O(log²n).
Morris threads a node to its inorder predecessor to climb back up without a stack. If you don't detect and REMOVE that temporary right-link on the second visit, you corrupt the tree (and loop forever). The thread must be undone.
Without explicit markers for null children, a string like 1,2,3 is ambiguous, many trees share it. Emit a sentinel (e.g. #) for every null so deserialize can rebuild the exact shape.
Returning max(rob, notRob) up the tree loses the information the parent needs (it must know the not-robbed value specifically). Return the full pair and only take the max at the very end (the root).
Eight techniques, one page. The right column is the phrase that should trigger each, the night-before surface for construction, threading and tree DP.
Structure gave you O(log²n) counting and O(1)-space flatten; running traversals backwards reconstructed and serialized the exact tree; Morris threaded a walk into O(1) space; and tree DP folded the whole tree up in one postorder. That completes Binary Trees across three decks. Next on the sheet: Binary Search Trees, where inorder's sorted property does most of the work.
Deck 3 of 3. Uses L32, L34-L38. L33 (unique-tree theory) folded into the construction intro. Morris is a concept unit. Added at the user's request: House Robber III, Binary Tree Cameras (the canonical tree DPs). BST (L39-L53) is a separate Step-14 deck.
Not a preference — an arithmetic one. Every slide is a fixed 1280 × 720 stage: a graph animating beside the code that drives it, with the problems laid out two columns wide. It scales as a single piece, so on a screen this size the body text comes out around 4px tall.
Shrinking it further would not help, and rebuilding it to reflow would mean losing the thing that makes it worth reading.