Skip to content

Commit 36baef6

Browse files
committed
feat(algorithms, dynamic-programming): house robber 3
1 parent 669d7ef commit 36baef6

23 files changed

+365
-28
lines changed

algorithms/dynamic_programming/house_robber/README.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,158 @@ Total amount you can rob = 2 + 9 + 1 = 12.
2626

2727
- Array
2828
- Dynamic Programming
29+
30+
---
31+
# House Robber III
32+
33+
The thief has found himself a new place for his thievery again. There is only one entrance to this area, called root.
34+
35+
Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that all houses
36+
in this place form a binary tree. It will automatically contact the police if two directly-linked houses were broken
37+
into on the same night.
38+
39+
Given the root of the binary tree, return the maximum amount of money the thief can rob without alerting the police.
40+
41+
## Constraints
42+
43+
- The number of nodes in the tree is in the range [1, 10^4].
44+
- 0 <= Node.val <= 10^4
45+
46+
## Examples
47+
48+
![Example 1](./images/examples/house_robber_3_example_1.png)
49+
![Example 1.1](./images/examples/house_robber_3_example_1.1.png)
50+
![Example 1.2](./images/examples/house_robber_3_example_1.2.png)
51+
![Example 1.3](./images/examples/house_robber_3_example_1.3.png)
52+
![Example 1.4](./images/examples/house_robber_3_example_1.4.png)
53+
![Example 2](./images/examples/house_robber_3_example_2.png)
54+
55+
![Example 3](./images/examples/house_robber_3_example_3.png)
56+
```text
57+
Input: root = [3,2,3,null,3,null,1]
58+
Output: 7
59+
Explanation: Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
60+
```
61+
62+
![Example 4](./images/examples/house_robber_3_example_4.png)
63+
```text
64+
Input: root = [3,4,5,1,3,null,1]
65+
Output: 9
66+
Explanation: Maximum amount of money the thief can rob = 4 + 5 = 9.
67+
```
68+
69+
## Topics
70+
71+
- Dynamic Programming
72+
- Tree
73+
- Depth-First Search
74+
- Binary Tree
75+
76+
## Solution(s)
77+
78+
1. [Recursion](#recursion)
79+
2. [Dynamic Programming(Memoization) - Top Down Approach](#dynamic-programmingmemoization-top-down-approach)
80+
3. [Dynamic Programming(Optimal)](#dynamic-programming-optimal--bottom-up-approach)
81+
82+
### Recursion
83+
84+
This is a tree version of the classic house robber problem. At each node, we have two choices: rob it or skip it. If we
85+
rob the current node, we cannot rob its immediate children, so we must skip to the grandchildren. If we skip the current
86+
node, we can consider robbing its children. We take the maximum of these two options.
87+
88+
#### Algorithm
89+
90+
- If the node is null, return 0.
91+
- Calculate the value if we rob the current node: add the node's value plus the result from its grandchildren
92+
(left.left, left.right, right.left, right.right).
93+
- Calculate the value if we skip the current node: add the results from robbing the left and right children.
94+
- Return the maximum of these two values.
95+
96+
#### Time Complexity
97+
98+
O(2^n)
99+
100+
#### Space complexity
101+
102+
O(n) for recursion stack.
103+
104+
### Dynamic Programming(Memoization) Top-Down Approach
105+
106+
The recursive solution recomputes results for the same nodes multiple times. By storing computed results in a cache
107+
(hash map), we avoid redundant work. Each node is processed at most once, significantly improving efficiency.
108+
109+
#### Algorithm
110+
111+
- Create a cache (hash map) to store computed results for each node.
112+
- Define a recursive function that checks the cache before computing.
113+
- If the node is in the cache, return the cached result.
114+
- Otherwise, compute the result using the same logic as the basic recursion: max of robbing current node
115+
(plus grandchildren) vs skipping current node (plus children).
116+
- Store the result in the cache and return it.
117+
118+
#### Complexity Analysis
119+
120+
- Time complexity: O(n)
121+
- Space complexity: O(n)
122+
123+
### Dynamic Programming (Optimal)- Bottom-Up Approach
124+
125+
Instead of caching all nodes, we can return two values from each subtree: the maximum if we rob this node, and the
126+
maximum if we skip it. This eliminates the need for a hash map. For each node, "with root" equals the node value plus
127+
the "without" values of both children. "Without root" equals the sum of the maximum values (either with or without) from
128+
both children.
129+
130+
#### Algorithm
131+
132+
- Define a recursive function that returns a pair: [maxWithNode, maxWithoutNode].
133+
- For a null node, return [0, 0].
134+
- Recursively get the pairs for left and right children.
135+
- Calculate withRoot as the node's value plus leftPair[1] plus rightPair[1] (children must be skipped).
136+
- Calculate withoutRoot as max(leftPair) plus max(rightPair) (children can be robbed or skipped).
137+
- Return [include_root, exclude_root].
138+
- The final answer is the maximum of the two values returned for the root.
139+
140+
![Solution 1](./images/solutions/house_robber_iii_solution_dp_bottom_up_1.png)
141+
![Solution 2](./images/solutions/house_robber_iii_solution_dp_bottom_up_2.png)
142+
![Solution 3](./images/solutions/house_robber_iii_solution_dp_bottom_up_3.png)
143+
![Solution 4](./images/solutions/house_robber_iii_solution_dp_bottom_up_4.png)
144+
![Solution 5](./images/solutions/house_robber_iii_solution_dp_bottom_up_5.png)
145+
![Solution 6](./images/solutions/house_robber_iii_solution_dp_bottom_up_6.png)
146+
![Solution 7](./images/solutions/house_robber_iii_solution_dp_bottom_up_7.png)
147+
![Solution 8](./images/solutions/house_robber_iii_solution_dp_bottom_up_8.png)
148+
![Solution 9](./images/solutions/house_robber_iii_solution_dp_bottom_up_9.png)
149+
![Solution 10](./images/solutions/house_robber_iii_solution_dp_bottom_up_10.png)
150+
![Solution 11](./images/solutions/house_robber_iii_solution_dp_bottom_up_11.png)
151+
![Solution 12](./images/solutions/house_robber_iii_solution_dp_bottom_up_12.png)
152+
153+
#### Complexity Analysis O(n)
154+
155+
The time complexity of this solution is O(n), where n is the number of nodes in the tree, since we visit all nodes once.
156+
157+
##### Space complexity: O(n)
158+
159+
The space complexity of this solution is O(n), since the maximum depth of the recursive call tree is the height of the
160+
tree. Which is n in the worst case, and each call stores its data on the stack.
161+
162+
### Common Pitfalls
163+
164+
#### Confusing "Skip to Grandchildren" with "Must Rob Grandchildren"
165+
166+
When robbing the current node, you cannot rob its immediate children, so you recursively consider the grandchildren.
167+
However, a common mistake is thinking you must rob the grandchildren. In reality, for each grandchild, you still have
168+
the choice to rob it or skip it. The recursive call on grandchildren will make this decision optimally. The constraint
169+
only prevents robbing adjacent nodes (parent-child), not skipping multiple levels.
170+
171+
#### Not Handling Null Children Properly
172+
173+
When calculating the value of robbing the current node, you need to add values from grandchildren. If a child is null,
174+
accessing child.left or child.right will cause a null pointer error. Always check if the left or right child exists
175+
before attempting to access their children. A null child contributes 0 to the total, and its non-existent children also
176+
contribute 0.
177+
178+
#### Forgetting to Take Maximum in the Final Answer
179+
180+
The optimal DFS solution returns a pair [withRoot, withoutRoot] representing the maximum money if we rob or skip the
181+
current node. At the root level, the final answer is the maximum of these two values, not just one of them. Forgetting
182+
to take this maximum and returning only withRoot or withoutRoot will give an incorrect result whenever the optimal
183+
strategy at the root differs from what you returned.

algorithms/dynamic_programming/house_robber/__init__.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from typing import List
1+
from typing import List, Optional, Dict
2+
3+
from datastructures.trees.binary.node import BinaryTreeNode
24

35

46
def rob(nums: List[int]) -> int:
@@ -10,3 +12,69 @@ def rob(nums: List[int]) -> int:
1012
current, previous = max(previous + house, current), current
1113

1214
return current
15+
16+
17+
def rob_iii_recursion(root: Optional[BinaryTreeNode]) -> int:
18+
if not root:
19+
return 0
20+
res = root.data
21+
22+
if root.left:
23+
res += rob_iii_recursion(root.left.left) + rob_iii_recursion(root.left.right)
24+
if root.right:
25+
res += rob_iii_recursion(root.right.left) + rob_iii_recursion(root.right.right)
26+
27+
res = max(res, rob_iii_recursion(root.left) + rob_iii_recursion(root.right))
28+
return res
29+
30+
31+
def rob_iii_dynamic_programming_top_down(root: Optional[BinaryTreeNode]) -> int:
32+
if not root:
33+
return 0
34+
35+
cache: Dict[BinaryTreeNode, int] = {}
36+
37+
def dfs(node: Optional[BinaryTreeNode]) -> int:
38+
if not node:
39+
return 0
40+
41+
if node in cache:
42+
return cache[node]
43+
44+
cache[node] = node.data
45+
46+
if node.left:
47+
cache[node] += dfs(node.left.left) + dfs(node.left.right)
48+
if node.right:
49+
cache[node] += dfs(node.right.left) + dfs(node.right.right)
50+
51+
cache[node] = max(cache[node], dfs(node.left) + dfs(node.right))
52+
return cache[node]
53+
54+
return dfs(root)
55+
56+
57+
def rob_iii_dynamic_programming_bottom_up(root: Optional[BinaryTreeNode]) -> int:
58+
if not root:
59+
return 0
60+
61+
def dfs(node: Optional[BinaryTreeNode]) -> List[int]:
62+
# Empty tree case
63+
if not node:
64+
return [0, 0]
65+
66+
# Recursively calculating the maximum amount that can be robbed from the left subtree of the root
67+
left_subtree = dfs(node.left)
68+
# Recursively calculating the maximum amount that can be robbed from the right subtree of the root
69+
right_subtree = dfs(node.right)
70+
71+
# include_root contains the maximum amount of money that can be robbed with the parent node included
72+
include_root = node.data + left_subtree[1] + right_subtree[1]
73+
74+
# exclude_root contains the maximum amount of money that can be robbed with the parent node excluded
75+
exclude_root = max(left_subtree) + max(right_subtree)
76+
77+
return [include_root, exclude_root]
78+
79+
# Returns maximum value from the pair: [include_root, exclude_root]
80+
return max(dfs(root))
41 KB
Loading
36.2 KB
Loading
40.8 KB
Loading
42.2 KB
Loading
40.3 KB
Loading
38.7 KB
Loading
41.5 KB
Loading
42.5 KB
Loading

0 commit comments

Comments
 (0)