diff --git a/algorithms/dynamic_programming/house_robber/README.md b/algorithms/dynamic_programming/house_robber/README.md index 21eeba5c..3652bd5b 100644 --- a/algorithms/dynamic_programming/house_robber/README.md +++ b/algorithms/dynamic_programming/house_robber/README.md @@ -26,3 +26,158 @@ Total amount you can rob = 2 + 9 + 1 = 12. - Array - Dynamic Programming + +--- +# House Robber III + +The thief has found himself a new place for his thievery again. There is only one entrance to this area, called root. + +Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that all houses +in this place form a binary tree. It will automatically contact the police if two directly-linked houses were broken +into on the same night. + +Given the root of the binary tree, return the maximum amount of money the thief can rob without alerting the police. + +## Constraints + +- The number of nodes in the tree is in the range [1, 10^4]. +- 0 <= Node.val <= 10^4 + +## Examples + +![Example 1](./images/examples/house_robber_3_example_1.png) +![Example 1.1](./images/examples/house_robber_3_example_1.1.png) +![Example 1.2](./images/examples/house_robber_3_example_1.2.png) +![Example 1.3](./images/examples/house_robber_3_example_1.3.png) +![Example 1.4](./images/examples/house_robber_3_example_1.4.png) +![Example 2](./images/examples/house_robber_3_example_2.png) + +![Example 3](./images/examples/house_robber_3_example_3.png) +```text +Input: root = [3,2,3,null,3,null,1] +Output: 7 +Explanation: Maximum amount of money the thief can rob = 3 + 3 + 1 = 7. +``` + +![Example 4](./images/examples/house_robber_3_example_4.png) +```text +Input: root = [3,4,5,1,3,null,1] +Output: 9 +Explanation: Maximum amount of money the thief can rob = 4 + 5 = 9. +``` + +## Topics + +- Dynamic Programming +- Tree +- Depth-First Search +- Binary Tree + +## Solution(s) + +1. [Recursion](#recursion) +2. [Dynamic Programming(Memoization) - Top Down Approach](#dynamic-programmingmemoization-top-down-approach) +3. [Dynamic Programming(Optimal)](#dynamic-programming-optimal--bottom-up-approach) + +### Recursion + +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 +rob the current node, we cannot rob its immediate children, so we must skip to the grandchildren. If we skip the current +node, we can consider robbing its children. We take the maximum of these two options. + +#### Algorithm + +- If the node is null, return 0. +- Calculate the value if we rob the current node: add the node's value plus the result from its grandchildren + (left.left, left.right, right.left, right.right). +- Calculate the value if we skip the current node: add the results from robbing the left and right children. +- Return the maximum of these two values. + +#### Time Complexity + +O(2^n) + +#### Space complexity + +O(n) for recursion stack. + +### Dynamic Programming(Memoization) Top-Down Approach + +The recursive solution recomputes results for the same nodes multiple times. By storing computed results in a cache +(hash map), we avoid redundant work. Each node is processed at most once, significantly improving efficiency. + +#### Algorithm + +- Create a cache (hash map) to store computed results for each node. +- Define a recursive function that checks the cache before computing. +- If the node is in the cache, return the cached result. +- Otherwise, compute the result using the same logic as the basic recursion: max of robbing current node + (plus grandchildren) vs skipping current node (plus children). +- Store the result in the cache and return it. + +#### Complexity Analysis + +- Time complexity: O(n) +- Space complexity: O(n) + +### Dynamic Programming (Optimal)- Bottom-Up Approach + +Instead of caching all nodes, we can return two values from each subtree: the maximum if we rob this node, and the +maximum if we skip it. This eliminates the need for a hash map. For each node, "with root" equals the node value plus +the "without" values of both children. "Without root" equals the sum of the maximum values (either with or without) from +both children. + +#### Algorithm + +- Define a recursive function that returns a pair: [maxWithNode, maxWithoutNode]. +- For a null node, return [0, 0]. +- Recursively get the pairs for left and right children. +- Calculate withRoot as the node's value plus leftPair[1] plus rightPair[1] (children must be skipped). +- Calculate withoutRoot as max(leftPair) plus max(rightPair) (children can be robbed or skipped). +- Return [include_root, exclude_root]. +- The final answer is the maximum of the two values returned for the root. + +![Solution 1](./images/solutions/house_robber_iii_solution_dp_bottom_up_1.png) +![Solution 2](./images/solutions/house_robber_iii_solution_dp_bottom_up_2.png) +![Solution 3](./images/solutions/house_robber_iii_solution_dp_bottom_up_3.png) +![Solution 4](./images/solutions/house_robber_iii_solution_dp_bottom_up_4.png) +![Solution 5](./images/solutions/house_robber_iii_solution_dp_bottom_up_5.png) +![Solution 6](./images/solutions/house_robber_iii_solution_dp_bottom_up_6.png) +![Solution 7](./images/solutions/house_robber_iii_solution_dp_bottom_up_7.png) +![Solution 8](./images/solutions/house_robber_iii_solution_dp_bottom_up_8.png) +![Solution 9](./images/solutions/house_robber_iii_solution_dp_bottom_up_9.png) +![Solution 10](./images/solutions/house_robber_iii_solution_dp_bottom_up_10.png) +![Solution 11](./images/solutions/house_robber_iii_solution_dp_bottom_up_11.png) +![Solution 12](./images/solutions/house_robber_iii_solution_dp_bottom_up_12.png) + +#### Complexity Analysis O(n) + +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. + +##### Space complexity: O(n) + +The space complexity of this solution is O(n), since the maximum depth of the recursive call tree is the height of the +tree. Which is n in the worst case, and each call stores its data on the stack. + +### Common Pitfalls + +#### Confusing "Skip to Grandchildren" with "Must Rob Grandchildren" + +When robbing the current node, you cannot rob its immediate children, so you recursively consider the grandchildren. +However, a common mistake is thinking you must rob the grandchildren. In reality, for each grandchild, you still have +the choice to rob it or skip it. The recursive call on grandchildren will make this decision optimally. The constraint +only prevents robbing adjacent nodes (parent-child), not skipping multiple levels. + +#### Not Handling Null Children Properly + +When calculating the value of robbing the current node, you need to add values from grandchildren. If a child is null, +accessing child.left or child.right will cause a null pointer error. Always check if the left or right child exists +before attempting to access their children. A null child contributes 0 to the total, and its non-existent children also +contribute 0. + +#### Forgetting to Take Maximum in the Final Answer + +The optimal DFS solution returns a pair [withRoot, withoutRoot] representing the maximum money if we rob or skip the +current node. At the root level, the final answer is the maximum of these two values, not just one of them. Forgetting +to take this maximum and returning only withRoot or withoutRoot will give an incorrect result whenever the optimal +strategy at the root differs from what you returned. diff --git a/algorithms/dynamic_programming/house_robber/__init__.py b/algorithms/dynamic_programming/house_robber/__init__.py index d97ed8a0..71ff235c 100644 --- a/algorithms/dynamic_programming/house_robber/__init__.py +++ b/algorithms/dynamic_programming/house_robber/__init__.py @@ -1,4 +1,6 @@ -from typing import List +from typing import List, Optional, Dict + +from datastructures.trees.binary.node import BinaryTreeNode def rob(nums: List[int]) -> int: @@ -10,3 +12,69 @@ def rob(nums: List[int]) -> int: current, previous = max(previous + house, current), current return current + + +def rob_iii_recursion(root: Optional[BinaryTreeNode]) -> int: + if not root: + return 0 + res = root.data + + if root.left: + res += rob_iii_recursion(root.left.left) + rob_iii_recursion(root.left.right) + if root.right: + res += rob_iii_recursion(root.right.left) + rob_iii_recursion(root.right.right) + + res = max(res, rob_iii_recursion(root.left) + rob_iii_recursion(root.right)) + return res + + +def rob_iii_dynamic_programming_top_down(root: Optional[BinaryTreeNode]) -> int: + if not root: + return 0 + + cache: Dict[BinaryTreeNode, int] = {} + + def dfs(node: Optional[BinaryTreeNode]) -> int: + if not node: + return 0 + + if node in cache: + return cache[node] + + cache[node] = node.data + + if node.left: + cache[node] += dfs(node.left.left) + dfs(node.left.right) + if node.right: + cache[node] += dfs(node.right.left) + dfs(node.right.right) + + cache[node] = max(cache[node], dfs(node.left) + dfs(node.right)) + return cache[node] + + return dfs(root) + + +def rob_iii_dynamic_programming_bottom_up(root: Optional[BinaryTreeNode]) -> int: + if not root: + return 0 + + def dfs(node: Optional[BinaryTreeNode]) -> List[int]: + # Empty tree case + if not node: + return [0, 0] + + # Recursively calculating the maximum amount that can be robbed from the left subtree of the root + left_subtree = dfs(node.left) + # Recursively calculating the maximum amount that can be robbed from the right subtree of the root + right_subtree = dfs(node.right) + + # include_root contains the maximum amount of money that can be robbed with the parent node included + include_root = node.data + left_subtree[1] + right_subtree[1] + + # exclude_root contains the maximum amount of money that can be robbed with the parent node excluded + exclude_root = max(left_subtree) + max(right_subtree) + + return [include_root, exclude_root] + + # Returns maximum value from the pair: [include_root, exclude_root] + return max(dfs(root)) diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.1.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.1.png new file mode 100644 index 00000000..0c2c34b0 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.1.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.2.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.2.png new file mode 100644 index 00000000..d8ea4720 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.2.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.3.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.3.png new file mode 100644 index 00000000..ae6c28d8 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.3.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.4.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.4.png new file mode 100644 index 00000000..8457be3d Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.4.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.png new file mode 100644 index 00000000..d8a87c8c Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_1.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_2.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_2.png new file mode 100644 index 00000000..5460304e Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_2.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_3.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_3.png new file mode 100644 index 00000000..f70fc4f5 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_3.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_4.png b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_4.png new file mode 100644 index 00000000..6375c3f6 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/examples/house_robber_3_example_4.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_1.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_1.png new file mode 100644 index 00000000..ea3f1b9f Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_1.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_10.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_10.png new file mode 100644 index 00000000..91ee72f2 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_10.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_11.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_11.png new file mode 100644 index 00000000..5b13d882 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_11.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_12.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_12.png new file mode 100644 index 00000000..6d33e981 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_12.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_2.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_2.png new file mode 100644 index 00000000..7ca419b3 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_2.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_3.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_3.png new file mode 100644 index 00000000..948dbda3 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_3.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_4.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_4.png new file mode 100644 index 00000000..e5c6afc2 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_4.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_5.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_5.png new file mode 100644 index 00000000..59ae2eae Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_5.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_6.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_6.png new file mode 100644 index 00000000..81964112 Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_6.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_7.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_7.png new file mode 100644 index 00000000..c8e7933d Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_7.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_8.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_8.png new file mode 100644 index 00000000..139607da Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_8.png differ diff --git a/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_9.png b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_9.png new file mode 100644 index 00000000..b875c08e Binary files /dev/null and b/algorithms/dynamic_programming/house_robber/images/solutions/house_robber_iii_solution_dp_bottom_up_9.png differ diff --git a/algorithms/dynamic_programming/house_robber/test_house_robber.py b/algorithms/dynamic_programming/house_robber/test_house_robber.py index f9a5c001..cb379e79 100644 --- a/algorithms/dynamic_programming/house_robber/test_house_robber.py +++ b/algorithms/dynamic_programming/house_robber/test_house_robber.py @@ -1,41 +1,155 @@ import unittest -from . import rob +from typing import List +from parameterized import parameterized +from algorithms.dynamic_programming.house_robber import ( + rob, + rob_iii_recursion, + rob_iii_dynamic_programming_top_down, + rob_iii_dynamic_programming_bottom_up, +) +from datastructures.trees.binary.node import BinaryTreeNode +ROB_TEST_CASES = [ + ([1, 2, 3, 1], 4), + ([2, 7, 9, 3, 1], 12), + ([], 0), + ([3], 3), + ([3, 5], 5), +] + +ROB_III_TEST_CASES = [ + ( + BinaryTreeNode( + data=3, + left=BinaryTreeNode(data=2, right=BinaryTreeNode(data=3)), + right=BinaryTreeNode(data=3, right=BinaryTreeNode(data=1)), + ), + 7, + ), + ( + BinaryTreeNode( + data=3, + left=BinaryTreeNode( + data=4, left=BinaryTreeNode(data=1), right=BinaryTreeNode(data=3) + ), + right=BinaryTreeNode(data=5, right=BinaryTreeNode(data=1)), + ), + 9, + ), + ( + BinaryTreeNode( + data=1, + left=BinaryTreeNode( + data=4, + left=BinaryTreeNode(data=2, left=BinaryTreeNode(data=3)), + right=BinaryTreeNode(data=3), + ), + ), + 7, + ), + ( + BinaryTreeNode( + data=1, + right=BinaryTreeNode( + data=2, + left=BinaryTreeNode( + data=3, left=BinaryTreeNode(data=4), right=BinaryTreeNode(data=2) + ), + right=BinaryTreeNode(data=5), + ), + ), + 12, + ), + ( + BinaryTreeNode( + data=3, + left=BinaryTreeNode(data=2, left=BinaryTreeNode(data=3)), + right=BinaryTreeNode(data=3, right=BinaryTreeNode(data=1)), + ), + 7, + ), + ( + BinaryTreeNode( + data=3, + left=BinaryTreeNode( + data=5, left=BinaryTreeNode(data=10), right=BinaryTreeNode(data=12) + ), + right=BinaryTreeNode( + data=25, left=BinaryTreeNode(data=3), right=BinaryTreeNode(data=1) + ), + ), + 47, + ), + ( + BinaryTreeNode( + data=9, + left=BinaryTreeNode( + data=7, left=BinaryTreeNode(data=1), right=BinaryTreeNode(data=8) + ), + right=BinaryTreeNode( + data=11, left=BinaryTreeNode(data=10), right=BinaryTreeNode(data=12) + ), + ), + 40, + ), + ( + BinaryTreeNode( + data=3, + ), + 3, + ), + ( + BinaryTreeNode( + data=8, + left=BinaryTreeNode(data=7), + right=BinaryTreeNode(data=10), + ), + 17, + ), + ( + BinaryTreeNode( + data=15, + left=BinaryTreeNode( + data=10, + left=BinaryTreeNode( + data=5, left=BinaryTreeNode(data=3), right=BinaryTreeNode(data=7) + ), + right=BinaryTreeNode( + data=12, left=BinaryTreeNode(data=11), right=BinaryTreeNode(data=13) + ), + ), + right=BinaryTreeNode( + data=25, left=BinaryTreeNode(data=20), right=BinaryTreeNode(data=30) + ), + ), + 99, + ), +] -class MyTestCase(unittest.TestCase): - def test_1(self): - """should return 4 for nums = [1,2,3,1]""" - nums = [1, 2, 3, 1] - expected = 4 - actual = rob(nums) - self.assertEqual(expected, actual) - def test_2(self): - """should return 4 for nums = [2,7,9,3,1]""" - nums = [2, 7, 9, 3, 1] - expected = 12 +class RobTestCase(unittest.TestCase): + @parameterized.expand(ROB_TEST_CASES) + def test_rob(self, nums: List[int], expected: int): actual = rob(nums) self.assertEqual(expected, actual) - def test_3(self): - """should return 0 for nums = []""" - nums = [] - expected = 0 - actual = rob(nums) + @parameterized.expand(ROB_III_TEST_CASES) + def test_rob_iii_recursion(self, root: BinaryTreeNode, expected: int): + actual = rob_iii_recursion(root) self.assertEqual(expected, actual) - def test_4(self): - """should return 3 for nums = [3]""" - nums = [3] - expected = 3 - actual = rob(nums) + @parameterized.expand(ROB_III_TEST_CASES) + def test_rob_iii_dynamic_programming_top_down( + self, root: BinaryTreeNode, expected: int + ): + actual = rob_iii_dynamic_programming_top_down(root) self.assertEqual(expected, actual) - def test_5(self): - """should return 3 for nums = [3, 5]""" - nums = [3, 5] - expected = 5 - actual = rob(nums) + @parameterized.expand(ROB_III_TEST_CASES) + def test_rob_iii_dynamic_programming_bottom_up( + self, root: BinaryTreeNode, expected: int + ): + actual = rob_iii_dynamic_programming_bottom_up(root) self.assertEqual(expected, actual)