Skip to content

LeetCode 198: House Robber

A clear explanation of maximizing robbery profit without robbing adjacent houses using dynamic programming.

Problem Restatement

We are given an integer array nums.

Each element represents the amount of money inside one house.

A robber wants to steal as much money as possible, but there is one restriction:

Two adjacent houses cannot both be robbed.

If two adjacent houses are robbed on the same night, the alarm system activates.

We need to return the maximum amount of money that can be robbed without triggering the alarm. The official problem asks for the maximum amount of money that can be robbed without robbing adjacent houses.

Input and Output

ItemMeaning
InputInteger array nums
OutputMaximum possible robbery amount
RestrictionCannot rob adjacent houses
GoalMaximize total money

Example function shape:

def rob(nums: list[int]) -> int:
    ...

Examples

Example 1:

nums = [1, 2, 3, 1]

Possible choices:

Houses robbedTotal
1 + 34
2 + 13
1 + 3 + 1invalid

The best valid choice is:

1 + 3 = 4

Answer:

4

Example 2:

nums = [2, 7, 9, 3, 1]

One good plan:

2 + 9 + 1 = 12

Answer:

12

First Thought: Try Every Combination

For every house, we have two choices:

ChoiceMeaning
Rob itSkip the next house
Skip itMove to the next house

This creates many possible combinations.

For example:

[2, 7, 9, 3]

We might choose:

2 + 9
7 + 3
2 + 3
9
...

The number of possibilities grows exponentially.

We need a way to reuse previous results.

Key Insight

At each house, there are only two meaningful possibilities:

OptionValue
Skip current houseBest answer up to previous house
Rob current houseCurrent money + best answer two houses back

Suppose we are at house i.

If we rob house i, then house i - 1 cannot be robbed.

So the total becomes:

nums[i] + best(i - 2)

If we skip house i, then the total becomes:

best(i - 1)

So the recurrence is:

dp[i]=max(dp[i1], dp[i2]+nums[i]) dp[i]=\max(dp[i-1],\ dp[i-2]+nums[i])

This is the core dynamic programming relation.

Algorithm

  1. If there are no houses, return 0.
  2. If there is one house, return its value.
  3. Use dynamic programming.
  4. For each house:
    • Either skip it.
    • Or rob it and add the best answer from two houses earlier.
  5. Return the final maximum value.

Building the DP Table

Let:

dp[i]

mean:

Maximum money we can rob from houses 0..i

Base cases:

dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])

Then for every later house:

dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

Correctness

For each house i, every valid robbery plan falls into one of two categories:

CategoryMeaning
House i is skippedBest value is dp[i - 1]
House i is robbedHouse i - 1 must be skipped, so value is dp[i - 2] + nums[i]

No valid plan exists outside these two cases.

The recurrence chooses the better of them.

So dp[i] correctly stores the maximum robbery amount for houses 0..i.

By induction, every DP state is correct, so the final result dp[n - 1] is also correct.

Complexity

MetricValueWhy
TimeO(n)We process each house once
SpaceO(n)The DP array stores one value per house

Here, n is the number of houses.

Implementation

class Solution:
    def rob(self, nums: list[int]) -> int:
        n = len(nums)

        if n == 0:
            return 0

        if n == 1:
            return nums[0]

        dp = [0] * n

        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])

        for i in range(2, n):
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

        return dp[-1]

Code Explanation

This handles the empty array case:

if n == 0:
    return 0

This handles a single house:

if n == 1:
    return nums[0]

We create the DP array:

dp = [0] * n

This initializes the first house result:

dp[0] = nums[0]

For two houses, we choose the larger value:

dp[1] = max(nums[0], nums[1])

Then we fill the remaining states:

for i in range(2, n):
    dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])

Finally:

return dp[-1]

returns the best possible robbery amount.

Space Optimization

Notice that:

dp[i]

depends only on:

dp[i - 1]
dp[i - 2]

So we do not need the full array.

We can keep only two variables.

Optimized Implementation

class Solution:
    def rob(self, nums: list[int]) -> int:
        prev2 = 0
        prev1 = 0

        for money in nums:
            current = max(prev1, prev2 + money)

            prev2 = prev1
            prev1 = current

        return prev1

Here:

VariableMeaning
prev1Best answer up to previous house
prev2Best answer up to two houses earlier

This reduces space complexity to O(1).

Testing

def run_tests():
    s = Solution()

    assert s.rob([1, 2, 3, 1]) == 4
    assert s.rob([2, 7, 9, 3, 1]) == 12
    assert s.rob([1]) == 1
    assert s.rob([2, 1, 1, 2]) == 4
    assert s.rob([0]) == 0
    assert s.rob([5, 1, 1, 5]) == 10

    print("all tests passed")

run_tests()

Test meaning:

TestWhy
[1,2,3,1]Main example
[2,7,9,3,1]Multiple optimal transitions
[1]Single house
[2,1,1,2]Forces skipping adjacent houses
[0]Zero-value house
[5,1,1,5]Best choice uses first and last houses