Lessons

Arrays

Dynamic Programming

Graph

Hashing

House Robber Problem

Imagine you’re a thief trying to rob houses along a street. However, there’s a constraint: robbing two neighbouring houses will alert the police! This setup defines the classic House Robber problem. Solving it requires smart optimization, and in this guide, we'll break it down step-by-step using dynamic programming, memoization, and effective state transitions.

Let’s dive into how you can maximize your loot, one smart move at a time!

What is the House Robber Problem?

The House Robber problem challenges you to select houses from a house sequence in a way that maximizes your total loot or max profit, without ever robbing two neighbouring houses.

Problem Statement:
Given an array where each element represents the amount of money in a house, determine the maximum amount you can rob without triggering the security systems by robbing adjacent houses.

Why Dynamic Programming is the Key

This problem is a textbook case for dynamic programming. Every decision (to rob a house or skip it) depends on previous decisions. Using a naive recursive solution would lead to a lot of repeated calculations, making it inefficient. That’s where memoization and state transition ideas make a huge difference!

Approaches to Solve the House Robber Problem

1. Recursive Solution

Let's first try a basic recursive solution. You either rob the current house and skip the next, or skip the current house entirely.

Code Example: Recursive Solution

python
1
2
3
4
5
6
7
8
def rob(houses, index=0):
    if index >= len(houses):
        return 0
    # Choose to rob or skip
    return max(
        houses[index] + rob(houses, index + 2),
        rob(houses, index + 1)
    )

Analysis:

  • Time Complexity: O(2^n) — Exponential because of repeated subproblems.
  • Space Complexity: O(n) due to recursion stack.

Drawback:

The recursive solution recalculates the same subproblems many times, leading to inefficiency. We need optimization.

2. Dynamic Programming with Memoization

By storing results of already-solved subproblems (memoization), we can greatly improve the performance.

Code Example: DP with Memoization

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def rob(houses):
    memo = {}

    def dp(index):
        if index >= len(houses):
            return 0
        if index in memo:
            return memo[index]

        result = max(
            houses[index] + dp(index + 2),
            dp(index + 1)
        )
        memo[index] = result
        return result

    return dp(0)

Key Concepts:

  • Use a dictionary memo to save the max profit at each step.
  • Avoid recomputing states already evaluated.

Complexity:

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

3. Bottom-Up Dynamic Programming

Another way to apply dynamic programming is using a bottom-up approach, calculating the result iteratively without recursion.

Code Example: Bottom-Up Approach

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def rob(houses):
    if not houses:
        return 0
    if len(houses) == 1:
        return houses[0]

    dp = [0] * len(houses)
    dp[0] = houses[0]
    dp[1] = max(houses[0], houses[1])

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

    return dp[-1]

How It Works:

  • Build up the solution using prior results.
  • Perform a state transition:
    • Either take the current house + profit two houses back
    • Or skip the current house and take the previous profit.

Complexity:

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

4. Space-Optimized Dynamic Programming

We can further improve by reducing the space complexity to O(1). We only need the last two results at any point.

Code Example: Space Optimization

python
1
2
3
4
5
6
7
8
9
10
def rob(houses):
    prev1 = 0
    prev2 = 0

    for money in houses:
        temp = prev1
        prev1 = max(money + prev2, prev1)
        prev2 = temp

    return prev1

Highlights:

  • Track only two variables instead of a full array.
  • Best choice for larger datasets.

Understanding State Transition

The key state transition formula is:

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

This simply means at each house, you either:

  • Rob it, and add the profit two houses back (since neighbouring is forbidden), or
  • Skip it and take the profit till the previous house.

Common Constraints in the House Robber Problem

When solving the House Robber problem, remember:

  • No robbing of two neighbouring houses.
  • All values are non-negative.
  • Input list might be empty.

These constraints drive the need for careful optimization.

Real-World Relevance

Although hypothetical, this problem teaches important problem-solving techniques:

  • Handling adjacent dependency problems.
  • Recognizing opportunities for dynamic programming.
  • Effective use of memoization for repeated subproblems.
  • Reducing algorithm complexity for real-time applications.

Conclusion

The House Robber Problem is a fantastic way to practice and master dynamic programming, optimization, and efficient state transitions. Starting with a simple recursive solution, improving it through memoization, and finally optimizing with dynamic programming are critical skills for every aspiring problem solver. Whether you choose recursion or the space-optimized approach, understanding these techniques will equip you to tackle many more challenging algorithmic problems!

Frequently Asked Questions