House Robber III – Solution & Complexity

Solution Walkthrough

1. Understand the constraint on the tree

  • Robbing a house forbids robbing its parent and its direct children.
  • At every node you face a binary choice: rob this node (and skip its children) or skip it (and freely choose within each child).
  • This is the linear House Robber idea lifted onto a tree.

2. Start from the naive recursion

  • If you rob the current node, add its value plus the best of each grandchild subtree.
  • If you skip it, add the best of each child subtree.
  • Take the larger of the two. This is correct but recomputes grandchildren over and over.
def rob(root: TreeNode) -> int:
    if root is None:
        return 0

    rob_this = root.val
    if root.left is not None:
        rob_this += rob(root.left.left) + rob(root.left.right)
    if root.right is not None:
        rob_this += rob(root.right.left) + rob(root.right.right)

    skip_this = rob(root.left) + rob(root.right)
    return max(rob_this, skip_this)

3. Return two values per node

  • Have each DFS call return a pair: the best loot if this node is robbed and the best if it is not.
  • robbed = node.val + skip(left) + skip(right) (children must be skipped).
  • notRobbed = max(child options) summed over both children.

4. Optimal single postorder pass

  • Combine both children's pairs in O(1) at each node, with no recomputation.
  • The answer is the better of the two options at the root.
def rob(root: TreeNode) -> int:
    def dfs(node: TreeNode | None) -> tuple[int, int]:
        if node is None:
            return (0, 0)
        left = dfs(node.left)
        right = dfs(node.right)
        rob_this = node.val + left[1] + right[1]
        skip_this = max(left) + max(right)
        return (rob_this, skip_this)

    return max(dfs(root))

5. Dry run

Trace root = [3,2,3,null,3,null,1].

node(rob, skip)
leaf 3 (under left 2)(3, 0)
left child 2(2, 3)
leaf 1 (under right 3)(1, 0)
right child 3(3, 1)
root 3(3 + 3 + 1, max(2,3)+max(3,1)) = (7, 6)

The answer is max(7, 6) = 7.

6. Common mistakes and follow-ups

  • Only tracking a single number per node, which forces expensive grandchild recomputation.
  • When robbing a node, adding the child's robbed value instead of its skipped value.
  • Forgetting that an empty tree returns 0.
  • Follow-up: how would you also return which houses were robbed?

7. Edge cases to test mentally

  • An empty tree yields 0; a single node yields its own value.
  • A straight chain alternates robbed/skipped levels.
  • Sometimes robbing every leaf beats robbing the root plus grandchildren, so always compare both options at the root.

8. Final full solution and complexity

A single postorder DFS returns each node's (robbed, skipped) best totals and combines children in O(1). Time is O(n) and stack space is O(h) for tree height h.

def rob(root: TreeNode) -> int:
    def dfs(node: TreeNode | None) -> tuple[int, int]:
        if node is None:
            return (0, 0)
        left = dfs(node.left)
        right = dfs(node.right)
        rob_this = node.val + left[1] + right[1]
        skip_this = max(left) + max(right)
        return (rob_this, skip_this)

    return max(dfs(root))

FAQ