The houses now form a binary tree, and robbing a node forbids robbing its direct children. One post-order DFS returns a pair [robThis, skipThis] per node — the linear rob-vs-skip state machine lifted onto a tree — for a single-pass, linear-time answer.
Houses are arranged as a binary tree. The thief can't rob two directly-linked houses (a parent and its child), or the alarm trips. Return the maximum money robbable. For root = [3,2,3,null,3,null,1] the answer is 7 — rob the root 3, plus the two grandchildren 3 and 1 (3 + 3 + 1 = 7); robbing the children 2 and 3 instead would force skipping both the root and grandchildren and yields less.
robThis (best for this subtree if we rob this node) and skipThis (best if we leave it). Robbing a node means its children must be skipped, so robThis = val + lSkip + rSkip. Skipping it frees each child to do whatever is best: skipThis = max(lRob, lSkip) + max(rRob, rSkip).dfs(null) = [0, 0]— an empty subtree loots nothing whether you "rob" it or not.[lRob, lSkip] and [rRob, rSkip] from the children beforescoring this node — you can't decide a node without its subtrees' two answers.robThis = node.val + lSkip + rSkip. Taking this house forces both children to be left alone, so each contributes its skip value only.skipThis = max(lRob, lSkip) + max(rRob, rSkip). With this house untouched, each child independently picks its better option.[robThis, skipThis] upward; at the root the answer is max(robRoot, skipRoot).lRob + rRob — a skipped node lets each child choose, so you must take max(lRob, lSkip) per child. And when you rob a node you may only add the children's skip values, never max(rob, skip) — otherwise you could illegally rob a parent and its child together.Returning the [rob, skip] pair fuses the two cases into a single visit, so time is O(n). Extra space is O(h) for the call stack, where h is the tree height — O(log n) balanced, O(n)for a degenerate chain. The naive "rob root vs skip root" recursion (with a separate helper) recomputes grandchildren exponentially; memoizing on the node fixes it, but the pair-return form needs no memo at all.
rob1/rob2track "best ending here vs one back"; here, the two array slots robThis/skipThis ride up the recursion instead of along an array. It's the same "hold both states, combine on the way" idea as Binary Tree Maximum Path Sum(return one value, record another) and any tree-DP where a node's choice constrains its children.[robThis, skipThis]: best loot of its subtree if we rob this node vs if we skip it. Children are solved before their parent.1class TreeNode {2 val: number;3 left: TreeNode | null;4 right: TreeNode | null;5 constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {6 this.val = val;7 this.left = left;8 this.right = right;9 }10}1112function rob(root: TreeNode | null): number {13 // returns [robThis, skipThis]:14 // robThis = best loot of this subtree IF we rob this node15 // skipThis = best loot of this subtree IF we leave this node alone16▶ function dfs(node: TreeNode | null): [number, number] {17 if (node === null) return [0, 0]; // empty: nothing either way1819 const [lRob, lSkip] = dfs(node.left); // children first (post-order)20 const [rRob, rSkip] = dfs(node.right);2122 // rob node => children MUST be skipped23▶ const robThis = node.val + lSkip + rSkip;24 // skip node => each child takes its own best (rob or skip)25▶ const skipThis = Math.max(lRob, lSkip) + Math.max(rRob, rSkip);2627 return [robThis, skipThis];28 }2930 const [robRoot, skipRoot] = dfs(root);31▶ return Math.max(robRoot, skipRoot); // root is free to rob or skip32}
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function rob(root: TreeNode | null): number {
// returns [robThis, skipThis]:
// robThis = best loot of this subtree IF we rob this node
// skipThis = best loot of this subtree IF we leave this node alone
function dfs(node: TreeNode | null): [number, number] {
if (node === null) return [0, 0]; // empty: nothing either way
const [lRob, lSkip] = dfs(node.left); // children first (post-order)
const [rRob, rSkip] = dfs(node.right);
// rob node => children MUST be skipped
const robThis = node.val + lSkip + rSkip;
// skip node => each child takes its own best (rob or skip)
const skipThis = Math.max(lRob, lSkip) + Math.max(rRob, rSkip);
return [robThis, skipThis];
}
const [robRoot, skipRoot] = dfs(root);
return Math.max(robRoot, skipRoot); // root is free to rob or skip
}dfs hands back a tuple [robThis, skipThis] for the subtree rooted at node. One number isn't enough: the parent needs to know the best both with and without this node robbed, because its own choice depends on it.dfs(null) returns [0, 0] — an empty subtree contributes nothing under either choice, so both slots are zero.[lRob, lSkip] and [rRob, rSkip]from the children before combining. You literally cannot fill this node's pair without both children's pairs.robThis = node.val + lSkip + rSkip. Robbing the parent bans robbing either child, so each child may contribute only its skip value.skipThis = max(lRob, lSkip) + max(rRob, rSkip). With the parent left alone, each child is unconstrained and picks its own better outcome.dfs(root), the root may itself be robbed or skipped — return Math.max(robRoot, skipRoot).O(n) time — each node is visited exactly once and does O(1) work — and O(h) recursion-stack space for tree height h(O(log n) balanced, O(n) worst case). No memoization needed: the pair return already encodes both subproblems.[rob, skip] pair preserves both cases so the parent can combine correctly.robThis or skipThis won at each node, then walk down from the root following the winning choice (and forcing children to skip whenever a parent was robbed).class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function rob(root: TreeNode | null): number {
// returns [robThis, skipThis]:
// robThis = best loot of this subtree IF we rob this node
// skipThis = best loot of this subtree IF we leave this node alone
function dfs(node: TreeNode | null): [number, number] {
if (node === null) return [0, 0]; // empty: nothing either way
const [lRob, lSkip] = dfs(node.left); // children first (post-order)
const [rRob, rSkip] = dfs(node.right);
// rob node => children MUST be skipped
const robThis = node.val + lSkip + rSkip;
// skip node => each child takes its own best (rob or skip)
const skipThis = Math.max(lRob, lSkip) + Math.max(rRob, rSkip);
return [robThis, skipThis];
}
const [robRoot, skipRoot] = dfs(root);
return Math.max(robRoot, skipRoot); // root is free to rob or skip
}O(n) time — each node is visited exactly once and does O(1) work — and O(h) recursion-stack space for tree height h(O(log n) balanced, O(n) worst case). No memoization needed: the pair return already encodes both subproblems.The direct translation of the choice: either rob this node and recurse on the grandchildren, or skip it and recurse on the children. Without a cache the grandchildren are re-solved exponentially; a per-node memo restores linear time.
class TreeNode {
val: number;
left: TreeNode | null;
right: TreeNode | null;
constructor(val = 0, left: TreeNode | null = null, right: TreeNode | null = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
function rob(root: TreeNode | null): number {
const memo = new Map<TreeNode, number>();
function best(node: TreeNode | null): number {
if (node === null) return 0;
const cached = memo.get(node);
if (cached !== undefined) return cached;
// Option A: rob node -> skip children, recurse on grandchildren.
let robIt = node.val;
if (node.left) robIt += best(node.left.left) + best(node.left.right);
if (node.right) robIt += best(node.right.left) + best(node.right.right);
// Option B: skip node -> take the best of each child.
const skipIt = best(node.left) + best(node.right);
const ans = Math.max(robIt, skipIt);
memo.set(node, ans);
return ans;
}
return best(root);
}memo the same subtrees are recomputed exponentially. The pair-return DFS encodes both the rob and skip outcomes in a single visit, so it needs no cache and touches each node exactly once.| House Robber but on a tree | post-order DFS returning [rob, skip] |
| robbing a node bans its children | robThis = val + lSkip + rSkip |
| node skipped → child free to choose | skipThis = max(lRob,lSkip)+max(rRob,rSkip) |
| parent choice depends on child choice | carry both states up the recursion |
function rob(root: TreeNode | null): number {
function dfs(node: TreeNode | null): [number, number] {
if (!node) return [0, 0];
const [lRob, lSkip] = dfs(node.left);
const [rRob, rSkip] = dfs(node.right);
const robThis = node.val + lSkip + rSkip;
const skipThis = Math.max(lRob, lSkip) + Math.max(rRob, rSkip);
return [robThis, skipThis];
}
return Math.max(...dfs(root));
}[robThis, skipThis]: the best loot of this subtree if we rob this node vs if we skip it.root = [3,2,3,null,3,null,1], what is the answer?