Code › algorithm-study

LeetCode 198 - House Robber

Solving the rob-or-skip choice with top-down DP and memoization

This is a Medium DP problem: return the maximum amount that can be robbed without choosing two adjacent houses. At each position, the decision is either rob the current house and skip the next one, or skip the current house and move to the next one.


  • Problem link: 198. House Robber
  • Summary: Given an integer array nums, return the maximum sum that can be collected without choosing adjacent elements.

For example:

nums = [1, 2, 3, 1]

Choosing the first and third houses gives a total of 4, and those houses are not adjacent. The answer for this input is 4.


Approach

The problem can be described as a choice at each house. If the current index is house and that house is robbed, the next valid index is house + 2. If the current house is skipped, the next index is house + 1.

I defined the recursive function to return the maximum amount that can be collected starting from a given house index.

rec(house) = max(nums[house] + rec(house + 2), rec(house + 1))

The left side of the max represents robbing the current house. The right side represents skipping it. Once house moves past the end of nums, there is nothing left to collect, so the function returns 0.

if house >= len(nums):
    return 0

Without memoization, the recursion keeps branching into two calls. From rec(0), robbing the current house skips house 1 and moves to rec(2). Skipping the current house moves to rec(1). So far, both branches match the problem rule directly.

flowchart TD R0(("rec(0)")) R2A(("rec(2)")) R1(("rec(1)")) R3(("rec(3)")) R2B(("rec(2)")) R0 -->|rob 0| R2A R0 -->|skip 0| R1 R1 -->|rob 1| R3 R1 -->|skip 1| R2B classDef normal fill:#172554,stroke:#60a5fa,color:#f8fafc; classDef duplicate fill:#422006,stroke:#f59e0b,color:#f8fafc; class R0,R1,R3 normal; class R2A,R2B duplicate;
Recursive branching without memoization, where rec(2) appears through multiple paths

At rec(1), the same choice happens again. Robbing house 1 moves to rec(3), while skipping house 1 moves to rec(2). That means rec(2) is needed both after “rob house 0 and skip house 1” and after “skip house 0 and skip house 1.”

The same index is therefore computed through multiple paths. As the depth grows, the call tree can almost double at each level, so plain recursion can grow to O(2ⁿ) time. The memo dictionary stores the best result for each house index and returns it immediately after the first calculation.

if house in memo:
    return memo[house]

This keeps the recursive structure while ensuring that each index is solved once.


Complexity analysis

  • Time complexity: O(n)

    • With memoization, each house index is computed once.
  • Space complexity: O(n)

    • The memo dictionary stores one result per index, and the recursion call stack can also grow linearly in the worst case.

Implementation code

from typing import Dict, List

class Solution:
    def rec(self, house: int, nums: List[int], memo: Dict[int, int]) -> int:
        if house >= len(nums):
            return 0

        if house in memo:
            return memo[house]

        current_robbed = max(
            nums[house] + self.rec(house + 2, nums, memo),
            self.rec(house + 1, nums, memo),
        )
        memo[house] = current_robbed
        return current_robbed

    def rob(self, nums: List[int]) -> int:
        memo: Dict[int, int] = {}
        return self.rec(0, nums, memo)

Summary and reflection

The main step is turning the rob-or-skip decision into a recurrence. Recursion alone repeats the same subproblems, so memoizing by house index turns the solution into top-down DP and keeps the time complexity at O(n).