337. House Robber III - Explanation

Problem Link

Description

The thief has found himself a new place for his thievery again. There is only one entrance to this area, called root.

In this new place, there are houses and each house has its only one parent house. All houses in this place form a binary tree. It will automatically contact the police if two directly-linked houses were broken.

You are given the root of the binary tree, return the maximum amount of money the thief can rob without alerting the police.

Example 1:

Input: root = [1,4,null,2,3,3]

Output: 7

Explanation: Maximum amount of money the thief can rob = 4 + 3 = 7

Example 2:

Input: root = [1,null,2,3,5,4,2]

Output: 12

Explanation: Maximum amount of money the thief can rob = 1 + 4 + 2 + 5 = 12

Constraints:

  • 1 <= The number of nodes in the tree <= 10,000.
  • 0 <= Node.val <= 10,000


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Binary Tree Traversal - Understanding how to recursively navigate tree structures (DFS)
  • Dynamic Programming on Trees - Extending DP concepts to tree-based problems
  • House Robber I - The foundational problem that this extends to tree structures
  • Memoization with Hash Maps - Caching results using node references as keys

1. Recursion

Intuition

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

  1. If the node is null, return 0.
  2. 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).
  3. Calculate the value if we skip the current node: add the results from robbing the left and right children.
  4. Return the maximum of these two values.
# 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:
        if not root:
            return 0

        res = root.val
        if root.left:
            res += self.rob(root.left.left) + self.rob(root.left.right)
        if root.right:
            res += self.rob(root.right.left) + self.rob(root.right.right)

        res = max(res, self.rob(root.left) + self.rob(root.right))
        return res

Time & Space Complexity

  • Time complexity: O(2n)O(2 ^ n)
  • Space complexity: O(n)O(n) for recursion stack.

2. Dynamic Programming (Memoization)

Intuition

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

  1. Create a cache (hash map) to store computed results for each node.
  2. Define a recursive function that checks the cache before computing.
  3. If the node is in the cache, return the cached result.
  4. 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).
  5. Store the result in the cache and return it.
# 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:
        cache = { None : 0 }

        def dfs(root):
            if root in cache:
                return cache[root]

            cache[root] = root.val
            if root.left:
                cache[root] += dfs(root.left.left) + dfs(root.left.right)
            if root.right:
                cache[root] += dfs(root.right.left) + dfs(root.right.right)

            cache[root] = max(cache[root], dfs(root.left) + dfs(root.right))
            return cache[root]

        return dfs(root)

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

3. Dynamic Programming (Optimal)

Intuition

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

  1. Define a recursive function that returns a pair: [maxWithNode, maxWithoutNode].
  2. For a null node, return [0, 0].
  3. Recursively get the pairs for left and right children.
  4. Calculate withRoot as the node's value plus leftPair[1] plus rightPair[1] (children must be skipped).
  5. Calculate withoutRoot as max(leftPair) plus max(rightPair) (children can be robbed or skipped).
  6. Return [withRoot, withoutRoot].
  7. The final answer is the maximum of the two values returned for the root.
# 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: TreeNode) -> int:
        def dfs(root):
            if not root:
                return [0, 0]

            leftPair = dfs(root.left)
            rightPair = dfs(root.right)

            withRoot = root.val + leftPair[1] + rightPair[1]
            withoutRoot = max(leftPair) + max(rightPair)

            return [withRoot, withoutRoot]

        return max(dfs(root))

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n) for recursion 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.