You are a thief planning to rob houses along a street. However, these houses are arranged in a binary tree structure, where each node represents a house, and the value of the node is the amount of money in that house. There's a catch: you cannot rob two directly linked houses because the alarm system will be triggered. Your goal is to maximize the total amount of money you can rob without setting off the alarm.
Details:
Example 1:
Consider the following binary tree:
3
/ \
2 3
\ \
3 1
The maximum amount you can rob is 3 (root) + 3 (right-child's right-child) + 1 (right-child's right-child) = 7.
Example 2:
Consider the following binary tree:
3
/ \
4 5
/ \ \
1 3 1
The maximum amount you can rob is 4 (left-child) + 5 (right-child) = 9.
Write a function that takes the root of the binary tree as input and returns the maximum amount of money the thief can rob without alerting the police.
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
We are trying to find the most money you can steal from houses arranged in a tree structure, where you can't rob adjacent houses. The brute force way involves checking every single possible combination of houses you could rob.
Here's how the algorithm would work step-by-step:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def house_robber_brute_force(root):
def rob_or_not_rob(current_node):
# If the node is empty, return 0
if not current_node:
return 0
# Case 1: Rob the current node
money_if_robbed = current_node.val
# If we rob the current node, we can't rob its children.
if current_node.left:
money_if_robbed += rob_or_not_rob_children(current_node.left, False)
if current_node.right:
money_if_robbed += rob_or_not_rob_children(current_node.right, False)
# Case 2: Don't rob the current node
money_if_not_robbed = 0
# If we don't rob the current node, we can rob its children.
if current_node.left:
money_if_not_robbed += rob_or_not_rob_children(current_node.left, True)
if current_node.right:
money_if_not_robbed += rob_or_not_rob_children(current_node.right, True)
return max(money_if_robbed, money_if_not_robbed)
def rob_or_not_rob_children(current_node, can_rob):
if not current_node:
return 0
if can_rob:
return rob_or_not_rob(current_node)
else:
# When we can't rob, we have to skip and recurse.
money_if_not_robbed = 0
if current_node.left:
money_if_not_robbed += rob_or_not_rob_children(current_node.left, True)
if current_node.right:
money_if_not_robbed += rob_or_not_rob_children(current_node.right, True)
return money_if_not_robbed
return rob_or_not_rob(root)
This problem asks us to find the maximum amount we can rob from houses arranged like a tree, where robbing adjacent houses sets off an alarm. The clever approach avoids redundant calculations by figuring out the best outcome for each house considering two options: robbing it, or skipping it.
Here's how the algorithm would work step-by-step:
def house_robber_iii(root):
def rob_sub_tree(node):
if not node:
return (0, 0)
# Recursively calculate for left and right subtrees
money_from_left = rob_sub_tree(node.left)
money_from_right = rob_sub_tree(node.right)
# If we rob the current node, we cannot rob its children
rob_current = node.val + money_from_left[1] + money_from_right[1]
# If we don't rob the current node, we can rob its children or not
skip_current = max(money_from_left) + max(money_from_right)
return (rob_current, skip_current)
# Return the maximum value between robbing the root and skipping it
return max(rob_sub_tree(root))
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# Example usage (for testing, not part of the core solution)
# root = TreeNode(3, TreeNode(2, None, TreeNode(3)), TreeNode(3, None, TreeNode(1)))
# print(house_robber_iii(root))
# root = TreeNode(3, TreeNode(4, TreeNode(1), TreeNode(3)), TreeNode(5, None, TreeNode(1)))
# print(house_robber_iii(root))
# This dictionary stores the results of subproblems
# to avoid redundant calculations.
# For each node, we calculate two values: the max
# loot if we rob the node, and the max loot if we skip it.
# We start from the bottom and go up, making decisions
# at each node based on the values calculated for its children.
Case | How to Handle |
---|---|
Null or empty tree | Return 0 if the root is null, representing no houses to rob. |
Single node tree | Return the value of the root node, as that's the only house to rob. |
Tree with all zero values | The algorithm should correctly return 0, as there's no profit to be made. |
Unbalanced tree (highly skewed to left or right) | Ensure the recursion depth doesn't exceed reasonable limits to prevent stack overflow. |
Tree with negative values (hypothetically, a house that costs you to rob) | The algorithm should still function correctly by choosing the path that maximizes the sum, even if that means avoiding negative values. |
Large tree with potential integer overflow (sum exceeds maximum integer value) | Consider using a larger data type (e.g., long) or modular arithmetic to prevent overflow if the house values can be large. |
Tree with a deep and narrow structure that leads to high call stack usage | Memoization or dynamic programming is important to improve the time complexity and prevent stack overflow errors. |
Tree where robbing the root provides no benefit because its children provide more value | The algorithm must correctly explore both robbing the root and not robbing the root to find the optimal solution. |