A clear explanation of House Robber III using tree dynamic programming with rob and skip states.
Problem Restatement
We are given the root of a binary tree.
Each node represents a house, and the value of the node is the amount of money in that house.
A thief wants to rob houses, but there is one rule:
If two directly connected houses are robbed, the police are alerted.In tree terms, we cannot rob both a parent node and its child node.
Return the maximum amount of money that can be robbed.
The problem constraints say the tree has between 1 and 10^4 nodes, and each node value is between 0 and 10^4.
Input and Output
| Item | Meaning |
|---|---|
| Input | Root of a binary tree |
| Output | Maximum money that can be robbed |
| Constraint | Cannot rob both a parent and its child |
| Allowed | Can rob grandchildren after robbing a node |
Function shape:
def rob(root: Optional[TreeNode]) -> int:
...Examples
Example 1:
Input: root = [3,2,3,null,3,null,1]
Output: 7The tree is:
3
/ \
2 3
\ \
3 1The best choice is:
3 + 3 + 1 = 7We rob the root and the two grandchildren.
Example 2:
Input: root = [3,4,5,1,3,null,1]
Output: 9The tree is:
3
/ \
4 5
/ \ \
1 3 1The best choice is:
4 + 5 = 9We skip the root and rob its children.
First Thought: Recursion With Choices
At each node, we have two choices:
| Choice | Result |
|---|---|
| Rob this node | Cannot rob its children |
| Skip this node | Children may be robbed or skipped |
A naive recursive solution may repeatedly recompute the same subtrees.
For example, when deciding whether to rob the root, we may compute values for children and grandchildren many times.
We need each subtree to give its parent enough information in one pass.
Key Insight
For every node, compute two values:
| State | Meaning |
|---|---|
rob_node | Maximum money if we rob this node |
skip_node | Maximum money if we skip this node |
If we rob the current node, we cannot rob its direct children.
So:
rob_node = node.val + left.skip + right.skipIf we skip the current node, each child can choose its better option.
So:
skip_node = max(left.rob, left.skip) + max(right.rob, right.skip)This is dynamic programming on a tree.
Algorithm
Use postorder DFS.
For each node:
- Recursively compute
(left_rob, left_skip)for the left child. - Recursively compute
(right_rob, right_skip)for the right child. - Compute the value if robbing the current node.
- Compute the value if skipping the current node.
- Return both values to the parent.
For a null node:
rob = 0
skip = 0At the end, the answer is:
max(root_rob, root_skip)Walkthrough
Use:
3
/ \
2 3
\ \
3 1Leaf node 3:
rob = 3
skip = 0Leaf node 1:
rob = 1
skip = 0Node 2 has no left child and right child 3.
If we rob 2:
rob = 2 + 0 + 0 = 2If we skip 2:
skip = max(0, 0) + max(3, 0) = 3So node 2 returns:
(2, 3)Node 3 on the right has right child 1.
If we rob it:
rob = 3 + 0 + 0 = 3If we skip it:
skip = max(0, 0) + max(1, 0) = 1So it returns:
(3, 1)Now process the root 3.
If we rob the root:
rob = 3 + left.skip + right.skip
= 3 + 3 + 1
= 7If we skip the root:
skip = max(2, 3) + max(3, 1)
= 3 + 3
= 6The answer is:
max(7, 6) = 7Correctness
For each node, there are only two possible states relevant to its parent: robbed or skipped.
If the current node is robbed, then its children cannot be robbed. The best value in that case is the current node value plus the skip values of both children.
If the current node is skipped, then each child is independent. For each child, we may choose whichever state gives more money.
The DFS computes these two values after computing the same two values for the children. This postorder traversal ensures every subtree is solved before its parent uses it.
The returned pair for a node therefore gives the optimal robbery amount for that subtree under both possible states of the node.
The root has no parent, so either state is allowed. Taking the maximum of the two root states gives the maximum amount of money that can be robbed without robbing any directly connected pair.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Each node is processed once |
| Space | O(h) | Recursion stack height, where h is tree height |
In the worst case, a skewed tree has height n, so recursion space can be O(n).
Implementation
from typing import Optional
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
def dfs(node: Optional[TreeNode]) -> tuple[int, int]:
if node is None:
return 0, 0
left_rob, left_skip = dfs(node.left)
right_rob, right_skip = dfs(node.right)
rob_node = node.val + left_skip + right_skip
skip_node = (
max(left_rob, left_skip)
+ max(right_rob, right_skip)
)
return rob_node, skip_node
root_rob, root_skip = dfs(root)
return max(root_rob, root_skip)Code Explanation
The DFS returns two values:
return rob_node, skip_nodeFor a missing child:
if node is None:
return 0, 0A null subtree contributes no money.
First compute the child states:
left_rob, left_skip = dfs(node.left)
right_rob, right_skip = dfs(node.right)If we rob the current node, we must skip both children:
rob_node = node.val + left_skip + right_skipIf we skip the current node, each child can choose its better state:
skip_node = max(left_rob, left_skip) + max(right_rob, right_skip)The final answer is the better root state:
return max(root_rob, root_skip)Testing
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def run_tests():
s = Solution()
root = TreeNode(
3,
TreeNode(2, None, TreeNode(3)),
TreeNode(3, None, TreeNode(1)),
)
assert s.rob(root) == 7
root = TreeNode(
3,
TreeNode(4, TreeNode(1), TreeNode(3)),
TreeNode(5, None, TreeNode(1)),
)
assert s.rob(root) == 9
root = TreeNode(5)
assert s.rob(root) == 5
root = TreeNode(
2,
TreeNode(1),
TreeNode(3),
)
assert s.rob(root) == 4
root = TreeNode(
4,
TreeNode(1, TreeNode(2), TreeNode(3)),
TreeNode(5),
)
assert s.rob(root) == 14
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| First sample | Rob root and grandchildren |
| Second sample | Skip root and rob children |
| Single node | Minimum tree |
| Root with two children | Choose root plus better child combination |
| Mixed tree | Checks independent subtree decisions |