Dynamic Programming Solutions for House Robber Problems: Linear, Circular, and Tree Variants

House Robber I

The classic house robber problem involves selecting houses to rob such that adjacent houses cannot both be robbed, maximizing total profit.

For each house, there are two choices: rob it or skip it. The decision at each position aims to maximize accumulated wealth.

State Defniition: wealth[i] represents the maximum money obtainable from houses 0 through i.

Recurrence Relation: For house i, either skip it (wealth[i-1]) or rob it (wealth[i-2] + houses[i]). Therefore: wealth[i] = max(wealth[i-1], wealth[i-2] + houses[i])

Initialization: wealth[0] = houses[0], wealth[1] = max(houses[0], houses[1])

// Time complexity: O(n)
// Space complexity: O(n)
class RobberySolution {
public:
    int calculateMaxProfit(vector<int>& houses) {
        if (houses.empty()) return 0;
        if (houses.size() == 1) return houses[0];
        
        vector<int> wealth(houses.size(), 0);
        wealth[0] = houses[0];
        wealth[1] = max(houses[0], houses[1]);
        
        for (int i = 2; i < houses.size(); i++) {
            wealth[i] = max(wealth[i-1], wealth[i-2] + houses[i]);
        }
        
        return wealth[houses.size() - 1];
    }
};

House Robber II

This variant introduces a circular arrangement where the first and last houses are adjacent, creating an additional constraint.

The solution separates into two scenarios: excluding the first house or excluding the last house. This prevents simultaneous selection of first and last houses.

// Time complexity: O(n)
// Space complexity: O(n)
class CircularRobberySolution {
private:
    int computeRange(vector<int>& houses, int start, int end) {
        if (start == end) {
            return houses[start];
        }
        
        vector<int> profits(houses.size());
        profits[start] = houses[start];
        profits[start + 1] = max(houses[start], houses[start + 1]);
        
        for (int i = start + 2; i <= end; i++) {
            profits[i] = max(profits[i-1], profits[i-2] + houses[i]);
        }
        
        return profits[end];
    }
    
public:
    int calculateMaxProfit(vector<int>& houses) {
        if (houses.size() == 1) return houses[0];
        
        int excludeLast = computeRange(houses, 0, houses.size() - 2);
        int excludeFirst = computeRange(houses, 1, houses.size() - 1);
        
        return max(excludeLast, excludeFirst);
    }
};

House Robber III

This version transforms the problem onto a binary tree structure, where adjacent nodes (parent-child) cannot both be selected.

The approach uses post-order traversal with state tracking for each node's inclusion/exclusion decisions.

State Representation: Each node returns a pair [skip_node_profit, take_node_profit]

  • Skip current node: Take maximum from left and right subtrees
  • Take current node: Add current value to profits from grandchildren
// Time complexity: O(n)
// Space complexity: O(log n)
class TreeRobberySolution {
private:
    vector<int> traverse(TreeNode* node) {
        if (!node) {
            return {0, 0}; // {skip_profit, take_profit}
        }
        
        vector<int> leftResults = traverse(node->left);
        vector<int> rightResults = traverse(node->right);
        
        int skipCurrent = max(leftResults[0], leftResults[1]) + 
                         max(rightResults[0], rightResults[1]);
        int takeCurrent = node->val + leftResults[0] + rightResults[0];
        
        return {skipCurrent, takeCurrent};
    }
    
public:
    int calculateMaxProfit(TreeNode* root) {
        vector<int> result = traverse(root);
        return max(result[0], result[1]);
    }
};

Alternative approaches include memoized recursion to avoid redundant calculations in the tree variant.

Tags: dynamic-programming algorithm tree-traversal memoization Recursion

Posted on Tue, 07 Jul 2026 17:10:21 +0000 by djelica