Structs & Data Structures · intermediate · ~15 min
Take-or-skip DP with an adjacency rule.
The house-robber problem asks for the maximum sum of a subset of array elements with the constraint that no two chosen elements are adjacent. The insight is that at each position you have exactly two states: include this element (and therefore take the best total that excluded its neighbour) or exclude it (and keep the best total so far either way). Two running variables carry those states, so the whole problem is a single pass in O(1) space. The circular variant — where the first and last elements are also adjacent — reduces neatly to two runs of the linear version.
Non-adjacent selection is a real constraint shape: scheduling jobs that cannot run back to back, choosing time slots with a mandatory gap, picking non-overlapping intervals, selecting sensors that must not be neighbours. The include/exclude state pair is also the simplest example of a DP whose state is not just an index — a pattern that generalises to far harder problems.
Two states per position. incl is the best total that does use the current element; excl is the best that does not. At each step: new_incl = excl + a[i] (using this element requires the previous to be excluded) and new_excl = max(incl, excl) (skipping it lets you keep whichever was better).
Order matters. Compute new_incl from the old excl before overwriting anything — updating in the wrong order silently allows adjacent picks.
The answer. max(incl, excl) at the end, since the optimum may or may not use the final element.
The circular variant. If the array is a ring, the first and last elements conflict. Because at most one of them can be chosen, the answer is max(rob(0..n-2), rob(1..n-1)) — two linear runs, each excluding one endpoint.
The n == 1 special case. Both ranges of the circular reduction are empty or malformed for a single element, so return a[0] directly.
Non-negative assumption. With negative values, "take nothing" may beat any selection; decide whether the empty set is allowed and seed accordingly.
/* best non-adjacent sum over a[lo..hi] inclusive */
long long rob(const int *a, int lo, int hi) {
long long incl = 0, excl = 0;
for (int i = lo; i <= hi; i++) {
long long ni = excl + a[i]; /* use OLD excl */
long long ne = (incl > excl) ? incl : excl;
incl = ni; excl = ne; /* commit together */
}
return (incl > excl) ? incl : excl;
}
long long rob_circular(const int *a, int n) {
if (n == 1) return a[0]; /* both ranges degenerate */
long long x = rob(a, 0, n - 2); /* drop the last */
long long y = rob(a, 1, n - 1); /* drop the first */
return (x > y) ? x : y;
}
Key points:
new_excl sees the already-updated incl.long long — sums of many elements overflow int easily.The house robber maximizes loot from a line of houses where you can't rob two adjacent ones — a clean take-or-skip DP. The circular variant (first and last are neighbours) reduces to two linear passes: rob everything but the last house, or everything but the first.
#include <stdio.h>
static long long rob(const int*a,int lo,int hi){long long incl=0,excl=0;for(int i=lo;i<=hi;i++){long long ni=excl+a[i];long long ne=incl>excl?incl:excl;incl=ni;excl=ne;}return incl>excl?incl:excl;}
static long long rob_circular(const int*a,int n){if(n==1)return a[0];long long x=rob(a,0,n-2),y=rob(a,1,n-1);return x>y?x:y;}
int main(void){
int houses[]={2,7,9,3,1};
printf("max loot, houses in a row = %lld\n", rob(houses,0,4));
printf("max loot, houses in a circle = %lld\n", rob_circular(houses,5));
return 0;
}
| Step | Line | What happens |
|---|---|---|
| 1 | incl = 0, excl = 0 |
Before any element, both states are empty. |
| 2 | i=0, a={2,7,9,3,1} |
ni = 0+2 = 2, ne = 0. Now incl=2, excl=0. |
| 3 | i=1 |
ni = excl+7 = 7 (skipping element 0), ne = max(2,0) = 2. |
| 4 | i=2 |
ni = 2+9 = 11 (uses element 0 and 2), ne = max(7,2) = 7. |
| 5 | i=3,4 |
Continues to incl=10, excl=11 then incl=12, excl=11. |
| 6 | return | max(12,11) = 12 — elements 2+9+1, none adjacent. |
Assuming 'every other house' is optimal; mishandling the circular first-last adjacency.
Compiler errors and warnings:
-Wsign-compare on the loop bounds; keep indices int.Runtime symptoms:
incl before computing excl (or used the new excl for incl). Compute both, then commit.n == 1. The rob(a, 0, -1) range is empty and rob(a, 1, 0) is malformed — special-case it.n == 2. Should return the larger element; verify the two ranges each cover exactly one element.long long.incl from the first element instead.Technique: test {2,7,9,3,1} (linear answer 12) and the same array circular (answer 11 — you cannot take both ends).
rob(a, lo, hi) reads a[lo..hi]; the circular caller must guarantee hi <= n-1 and lo >= 0. The n == 1 guard exists precisely because n-2 would be -1.lo > hi the loop body never runs and the function returns 0 — safe, but make sure that is the answer you want.long long accumulators plus a bounded input range keep the arithmetic well defined.const int *a documents that the input is not modified and lets the compiler catch accidental writes.Concrete uses: Scheduling tasks that require a cooldown between runs. Choosing advertising slots that cannot be back to back. Selecting non-adjacent sensors or cell towers to avoid interference. Picking non-overlapping time intervals for maximum value. The circular variant models schedules that wrap around a day or a ring topology.
Professional best practices:
Beginner:
Intermediate:
1. (Beginner) Linear robber. Implement long long rob(const int *a, int lo, int hi) with the include/exclude pair. Example: {2,7,9,3,1} -> 12. Concepts: two-state DP, O(1) space.
2. (Beginner) Circular robber. Implement rob_circular using two linear runs. Requirements: handle n == 1. Example: {2,3,2} -> 3. Concepts: constraint reduction.
3. (Intermediate) Report the selection. Extend the linear version to output which indices were chosen. Hint: this needs a table, not two scalars — record the decision at each step. Concepts: the space/information trade-off.
4. (Intermediate) Minimum gap of two. Generalise so chosen elements must be at least three apart. Hint: the include state now draws from two positions back. Concepts: widening the state.
Non-adjacent selection is a two-state DP: incl (best total using the current element) comes from the previous excl plus this value, while excl keeps the better of the two previous states. Compute both before committing either, or the update silently permits adjacent picks. The answer is the larger of the two at the end, and the whole thing runs in one pass with no allocation. A circular array reduces to two linear runs — one dropping the first element, one dropping the last — with n == 1 handled separately.