Definition
- House Robber – a one-dimensional decision DP where each element has a binary choice: take or skip.
- Recurrence –
dp[i] = max(dp[i-1], nums[i] + dp[i-2]), encoding that robbing house i forbids house i-1. - Rolling variables – since dp[i] depends only on the two preceding values, two variables replace the full array.
- Time – O(n) with a single pass through the array.
- Space – O(1) when using two rolling variables instead of an array.
The National Bank of India selects non-adjacent time slots for high-value promotional campaigns. Two back-to-back slots conflict because customers get fatigued. At each slot, the bank either skips it and keeps the best revenue so far, or takes it and adds its value to the best revenue from two slots ago. Whichever is larger becomes the new running best.
Understanding the Concepts
What is the House Robber problem?
The House Robber problem gives you an array of non-negative integers where each element represents the value stored in a house along a street. You want to maximize the total value you collect, but you cannot collect from two adjacent houses. If you take house i, you must skip house i-1 and house i+1.
In banking terms, the National Bank of India picks non-adjacent promotional time slots to maximize total campaign revenue without fatiguing customers with back-to-back promotions.
Why does a DP approach work here?
At each house you face a binary decision: rob it or skip it. If you rob it, you earn its value plus the best you could achieve up to two houses back (since the adjacent house is forbidden). If you skip it, you carry forward the best you could achieve up to the previous house. This gives overlapping subproblems with optimal substructure, the two hallmarks of dynamic programming.
A greedy approach fails because taking the locally largest house can block access to a pair of houses that together yield more.
// Greedy fails: houses = [2, 7, 9, 3, 1]
// Greedy picks 9 (largest), skips 7 and 3, then picks 2 and 1 = 12
// Optimal: 2 + 9 + 1 = 12 (same here, but consider [2, 1, 1, 2])
// Greedy picks 2 (first or last), misses 2+2=4
How does the recurrence work?
The recurrence is dp[i] = Math.max(dp[i-1], nums[i] + dp[i-2]). The first term is the skip case: the best total through house i-1 carries forward. The second term is the rob case: the value of house i plus the best total through house i-2, because robbing i forbids i-1.
Base cases: dp[0] = nums[0] (only one house, rob it) and dp[1] = Math.max(nums[0], nums[1]) (two houses, pick the better one).
package com.bank.service;
public class PromotionSlotSelector {
public static int maxRevenue(int[] slotValues) {
if (slotValues == null || slotValues.length == 0) return 0;
if (slotValues.length == 1) return slotValues[0];
int[] dp = new int[slotValues.length];
dp[0] = slotValues[0];
dp[1] = Math.max(slotValues[0], slotValues[1]);
for (int i = 2; i < slotValues.length; i++) {
dp[i] = Math.max(dp[i - 1], slotValues[i] + dp[i - 2]);
}
return dp[slotValues.length - 1];
}
}
How do you optimise space with rolling variables?
Since dp[i] depends only on dp[i-1] and dp[i-2], you replace the entire array with two variables. This reduces space from O(n) to O(1).
package com.bank.service;
public class PromotionSlotSelectorOptimised {
public static int maxRevenue(int[] slotValues) {
if (slotValues == null || slotValues.length == 0) return 0;
if (slotValues.length == 1) return slotValues[0];
int prev2 = slotValues[0];
int prev1 = Math.max(slotValues[0], slotValues[1]);
for (int i = 2; i < slotValues.length; i++) {
int current = Math.max(prev1, slotValues[i] + prev2);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
public static void main(String[] args) {
int[] slots = {200, 700, 900, 300, 100};
System.out.println("Max revenue: " + maxRevenue(slots));
}
}
Max revenue: 1100
How is House Robber different from House Robber II (circular)?
| Aspect | House Robber (Linear) | House Robber II (Circular) |
|---|---|---|
| Layout | Houses in a line | Houses in a circle |
| Constraint | Cannot rob adjacent | Cannot rob adjacent, first and last are also adjacent |
| Solution | Single linear DP pass | Two linear DP passes, take the max |
| Passes | One pass over all houses | Pass 1: exclude last house. Pass 2: exclude first house |
In the circular version, the first and last houses are adjacent, so you cannot rob both. The trick is to run the linear House Robber twice: once on houses 0 through n-2 and once on houses 1 through n-1, then return the larger result.
public static int maxRevenueCircular(int[] slotValues) {
int n = slotValues.length;
if (n == 1) return slotValues[0];
return Math.max(
maxRevenueRange(slotValues, 0, n - 2),
maxRevenueRange(slotValues, 1, n - 1)
);
}
private static int maxRevenueRange(int[] values, int start, int end) {
int prev2 = 0, prev1 = 0;
for (int i = start; i <= end; i++) {
int current = Math.max(prev1, values[i] + prev2);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
What are the edge cases?
Edge cases include empty arrays, single-element arrays, and arrays where all values are equal.
- Empty array – return 0, there is nothing to rob.
- Single house – return its value, no adjacency constraint applies.
- Two houses – return the larger of the two.
- All values equal – the answer is the sum of every other element, roughly n/2 times the value.
- All zeros except one – the answer is that single non-zero value.
// Edge case: two houses
int[] twoSlots = {500, 300};
// dp[0] = 500, dp[1] = max(500, 300) = 500
// Answer: 500 (skip the smaller slot)
What are common mistakes?
The most common mistake is initialising dp[1] incorrectly.
// WRONG: dp[1] = nums[1]
// This ignores the possibility that nums[0] > nums[1]
dp[1] = slotValues[1]; // BUG
// CORRECT: dp[1] = max(nums[0], nums[1])
dp[1] = Math.max(slotValues[0], slotValues[1]);
Another mistake is reaching back to dp[i-2] when i is 1 without handling the base case, causing an index-out-of-bounds error.
What is the time and space complexity?
The array version runs in O(n) time and O(n) space. The rolling-variable version runs in O(n) time and O(1) space. Both make a single pass through the input. For a banking promotion scheduler handling hundreds of time slots, this is instant.
What should a senior engineer know?
A senior engineer should recognise that House Robber is a template for all "non-adjacent selection" problems. The same pattern applies to maximising values on a number line with exclusion zones, job scheduling where consecutive jobs conflict, and selecting non-overlapping intervals.
The recurrence dp[i] = max(dp[i-1], value[i] + dp[i-2]) generalises to dp[i] = max(dp[i-1], value[i] + dp[i-k]) when the exclusion zone is k elements wide instead of one.
// Generalised: cannot select within k positions
public static int maxRevenueWithGap(int[] values, int gap) {
int n = values.length;
int[] dp = new int[n];
for (int i = 0; i < n; i++) {
int skip = (i > 0) ? dp[i - 1] : 0;
int take = values[i] + ((i > gap) ? dp[i - gap - 1] : 0);
dp[i] = Math.max(skip, take);
}
return dp[n - 1];
}
Visual Overview
House Robber Decision Flow
Houses: [200] [700] [900] [300] [100]
Index: 0 1 2 3 4
At each house i:
┌──────────────────────────────────────────┐
│ Option 1: SKIP house i │
│ best = dp[i-1] (carry forward) │
│ │
│ Option 2: ROB house i │
│ best = nums[i] + dp[i-2] (skip i-1) │
│ │
│ dp[i] = max(Option 1, Option 2) │
└──────────────────────────────────────────┘
Trace:
dp[0] = 200
dp[1] = max(200, 700) = 700
dp[2] = max(700, 900+200) = 1100
dp[3] = max(1100, 300+700) = 1100
dp[4] = max(1100, 100+1100) = 1200
^
Wait: dp[4] = max(1100, 100+1100) = 1200
But dp[i-2] for i=4 is dp[2]=1100
So 100+1100 = 1200 > 1100 -> dp[4] = 1200
Rolling Variables:
┌────────┐ ┌────────┐ ┌────────┐
│ prev2 │───>│ prev1 │───>│ cur │
│ dp[i-2]│ │ dp[i-1]│ │ dp[i] │
└────────┘ └────────┘ └────────┘
shift left after each step
Key Takeaway
House Robber is a one-dimensional decision DP. At each house you face a binary choice: skip it, in which case the best total is whatever you had through the previous house, or rob it, in which case you earn its value plus the best total from two houses back, because robbing it forbids the immediately adjacent house. The recurrence is dp[i] = max(dp[i-1], nums[i] + dp[i-2]).
Since each state depends only on the two preceding values, you keep two rolling variables rather than an array, giving linear time and constant space. The subtle point interviewers probe is why robbing reaches back two positions rather than one, and the answer is precisely the adjacency constraint: taking house i blocks house i-1, so the most recent safe state is house i-2.
In the National Bank of India project, marketing chose non-adjacent promotional time slots to maximize total campaign revenue, since consecutive slots would fatigue customers. This is structurally identical to House Robber. For the circular case (House Robber II), where the first and last slots are also adjacent, Rajesh ran the linear solution twice, once excluding the first slot and once excluding the last, and took the larger result.
The senior insight is that House Robber is a template for all non-adjacent selection problems. The same max(skip, take + lookback) pattern recurs in job scheduling, interval selection, and any problem where choosing one element excludes its neighbours. Recognising this pattern and reducing space with rolling variables is what interviewers expect at the senior level.
Remember one sentence: House Robber decides at each position whether to skip (carry forward dp[i-1]) or take (value plus dp[i-2]), and rolling two variables gives O(n) time and O(1) space.
---