Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Pick a subset with maximum sum where no two chosen elements are adjacent (you may skip freely). The recurrence is dp[i] = max(dp[i-1], dp[i-2] + a[i], a[i]): skip i, take i on top of the best up to i-2, or start fresh at i. This is the House Robber pattern generalised to allow skipping and negatives. O(n) time, and O(1) space once you keep only the last two dp values.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you model 'no two adjacent' as a take-or-skip recurrence depending on dp[i-1] and dp[i-2], and whether you handle negatives by allowing an element to stand alone.
Where candidates slip
Dropping the `a[i]` (start-fresh) term, so a negative prefix drags down an otherwise good single element; or using dp[i-1] instead of dp[i-2] when taking i (which permits adjacency).
How to stand out
Name the House Robber pattern, derive the recurrence from the binary take/skip choice, then collapse the dp array to O(1) space with two rolling variables.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewAt index iTake a[i]?SkipTakeExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 3
Brute Force — Recurse Over Take / Skip
1Hint 1Explore hintHide hint
Stand at index i. You can either take a[i] — then the next allowed index is i+2 — or skip it and move to i+1. What's the best you can do from index i onward?
2Hint 2Explore hintHide hint
Write f(i) = max(a[i] + f(i+2), f(i+1)): take this element and jump two, or skip and step one.
3Hint 3Explore hintHide hint
This explores both branches at every index → 2ⁿ paths. Notice f(i) is recomputed many times for the same i — that's the cue for memoization.
A · Core idea
The most literal model: at every index decide independently to take or skip. If you take a[i], you must jump to i+2 because i+1 is now forbidden. If you skip, you advance to i+1 with no penalty. The answer from index i onward is the better of the two branches: f(i) = max(a[i] + f(i+2), f(i+1)).
Why two branches and nothing else. The adjacency rule constrains only immediate neighbours. Once you decide about index i, the rest of the problem is the same shape on a shorter suffix — a smaller instance of Max Array Sum. That self-similarity is what makes it a DP problem rather than a greedy one.
Why greedy fails. A tempting greedy is 'always take the larger of two neighbours'. It breaks on [2, 1, 5, 8, 4]: greedy might grab 8, blocking both 5 and 4, but the optimal 2 + 5 + 4 = 11 beats 1 + 8 = 9. Local choices can't see the downstream cost, so you must compare full branches — hence recursion.
Why we don't ship this. The recursion tree has ~`2ⁿnodes, but there are onlyndistinct arguments tof. The same f(i)is recomputed exponentially often. Forn = 10⁵this never finishes. The fix is to *remember* eachf(i)` — memoization (top-down) or tabulation (bottom-up).
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
a = [3, 7, 4]Interactive diagram
The recursion tree recomputes f(i)
f(2)Notice f(2) appears on multiple branches — that repetition is the exponential cost.
7C · Code with teaching comments
Reference implementation previewpublic int maxSubsetSum(int[] a) { return f(a, 0); } private int f(int[] a, int i) {Explore the full codeHide the full code
D · Why it works and complexityO(2ⁿ) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
The exponential cost comes entirely from recomputing the same f(i) over and over. There are only n distinct subproblems, so caching each one collapses 2ⁿ work down to O(n). Spotting 'few distinct subproblems, recomputed often' is the universal trigger for dynamic programming.
ComplexityO(2ⁿ) time, O(n) space — Each call spawns two recursive calls (take / skip), forming a binary tree of depth n → roughly 2ⁿ calls. Recursion depth is O(n) for the call stack.
E · Edge cases and pitfalls3Recursing to i+1 after taking a[i] — that permits adjacency and is wrong.Explore edge casesHide edge cases
Pitfalls
- Recursing to i+1 after taking a[i] — that permits adjacency and is wrong.
- Forgetting the base case, causing infinite recursion or index errors.
- Running it on large n — 2ⁿ never returns; you must memoize or tabulate.
Approach 2 of 3
Top-Down Memoization (Java) vs Bottom-Up Tabulation (Python)
1Hint 1Explore hintHide hint
There are only n distinct values of f(i). Store each one the first time you compute it so the second request is a lookup, not a recomputation.
2Hint 2Explore hintHide hint
Top-down: keep the recursion but cache results in an array indexed by i. Bottom-up: drop recursion and fill dp[0], dp[1], ... in order using dp[i] = max(dp[i-1], dp[i-2] + a[i], a[i]).
3Hint 3Explore hintHide hint
Both visit each subproblem once → O(n) time and O(n) space. The only real difference is recursion-stack risk (top-down) versus an explicit loop (bottom-up).
A · Core idea
Same recurrence, two equivalent implementations — and they're deliberately shown in different languages to highlight the trade-off.
Java — top-down memoization. Keep the natural recursion from the brute force, but back it with a memo array. Before computing f(i), check whether it's already cached; after computing, store it. Each f(i) is now evaluated once, so the 2ⁿ tree collapses to n real computations.
Python — bottom-up tabulation. Drop recursion entirely. Fill a dp array left to right: dp[i] = max(dp[i-1], dp[i-2] + a[i], a[i]). Because dp[i-1] and dp[i-2] are computed before dp[i], every value is ready when needed. The answer is dp[n-1].
Why they're the same algorithm. Both compute exactly the set of n subproblems once. Memoization computes them lazily, in the order the recursion happens to ask; tabulation computes them eagerly, in a fixed left-to-right order. Same time, same space, same answers.
Where they differ — and why it matters here. Top-down recursion uses the call stack: for n = 10⁵, deep recursion can blow the stack. In Python this is acute — the default recursion limit is ~1000, so a top-down solution would raise RecursionError on large inputs unless you raise the limit. That's exactly why Python is shown bottom-up: tabulation uses a plain loop and no stack depth. Java tolerates deeper recursion and the JIT handles it well, so top-down reads cleanly there. The lesson: when n is large, prefer tabulation (or an explicit stack) in stack-limited languages.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
a = [3, 7, 4, 6, 5]Array state
i=1
dp[1] = max(3, 7) = 7
13C · Code with teaching comments
Reference implementation previewpublic int maxSubsetSum(int[] a) { Integer[] memo = new Integer[a.length]; return f(a, 0, memo); } Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Memoization and tabulation are two views of one DP: lazy versus eager evaluation of the same subproblem set. Reach for top-down when the recurrence is easier to write recursively, and bottom-up when recursion depth (especially in Python) would overflow the stack. Knowing both — and when each fails — is what separates a memorised solution from an understood one.
ComplexityO(n) time, O(n) space — Each of the n subproblems is solved exactly once and cached, so total work is O(n). The cache (memo array or dp table) holds n entries → O(n) space. Top-down also uses O(n) call-stack depth.
E · Edge cases and pitfalls3In Java, using int[] for the memo can't tell 'unset' from a real 0 — use Integer[] with null.Explore edge casesHide edge cases
Pitfalls
- In Java, using int[] for the memo can't tell 'unset' from a real 0 — use Integer[] with null.
- In Python, writing this top-down risks RecursionError at n = 10⁵ — tabulate or raise the recursion limit.
- Forgetting dp[1] = max(a[0], a[1]); setting dp[1] = a[1] allows skipping a better a[0].
Approach 3 of 3 · Optimal
Rolling Variables — O(1) Space (Optimal)
1Hint 1Explore hintHide hint
Look at the recurrence: dp[i] depends only on dp[i-1] and dp[i-2]. Do you ever read anything older?
2Hint 2Explore hintHide hint
Replace the whole dp array with two variables — prev1 (dp[i-1]) and prev2 (dp[i-2]) — and slide them forward each step.
3Hint 3Explore hintHide hint
Initialise prev2 = 0 and prev1 = a[0]. Each step: cur = max(prev1, prev2 + a[i], a[i]); then prev2 = prev1, prev1 = cur.
A · Core idea
The tabulation never reaches further back than two cells, so storing the entire dp array wastes memory. Keep just two rolling variables: prev2 = dp[i-2] and prev1 = dp[i-1]. Compute cur = max(prev1, prev2 + a[i], a[i]), then slide: prev2 becomes prev1, prev1 becomes cur.
Why the three terms, restated for the scalar form. prev1 is the skip choice (carry the best up to i-1). prev2 + a[i] is the take choice (add a[i] to the best that excluded i-1). a[i] alone is the fresh-start choice — essential when prev2 is negative, so a strong single element isn't dragged down by a bad prefix.
Why `prev2` starts at 0. At i = 1, the term prev2 + a[1] should mean 'take a[1] with nothing before it', i.e. just a[1]. Seeding prev2 = 0 makes that fall out automatically, and it equals the a[i] term — harmless overlap.
Why this is the version to ship. Same O(n) time as tabulation but O(1) space, and no recursion stack to overflow — safe even at n = 10⁵. It's also the cleanest to state in an interview: two variables, one pass.
Reconstructing the chosen subset. If asked which elements were picked, the O(1) trick loses that information — you'd keep the full dp array (or a parent/decision array) and walk backwards. Mention this trade-off: O(1) space gives the value, not the witness.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
a = [2, 1, 5, 8, 4]Array state
i=1
cur = max(2, 1, 1) = 2 → prev1=2
11C · Code with teaching comments
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
A recurrence whose window reaches back a fixed number of cells (here two) can always shed its array for that many scalars. This is the same compression that turns Fibonacci and Climbing Stairs from O(n) space into O(1) — recognise the bounded look-back and the optimisation is automatic.
ComplexityO(n) time, O(1) space — A single pass over n elements with constant work per step → O(n) time. Only two scalars are retained regardless of n → O(1) space.
E · Edge cases and pitfalls6a = [5] — Loop never runs; prev1 = a[0] = 5 is returned. Single element is its own answer.Explore edge casesHide edge cases
Edge cases handled
a = [5]Loop never runs; prev1 = a[0] = 5 is returned. Single element is its own answer.a = [-1, -2, -3]Every take term is negative; cur tracks the least-bad single element → returns -1. The a[i] term keeps it from compounding negatives.a = [1, 2, 3]i=1: max(1,2,2)=2; i=2: max(2, 1+3, 3)=4 → returns 4 (take 1 and 3). Adjacency between 2 and 3 correctly forbidden.
Pitfalls
- Dropping the a[i] term — negative prev2 then forces a worse sum than taking the element alone.
- Using prev1 (dp[i-1]) instead of prev2 in the take branch — that allows adjacent picks.
- Trying to recover the chosen indices from O(1) space — you need the full dp array for that.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int maxSubsetSum(int[] a) { int prev2 = 0; int prev1 = a[0]; for (int i = 1; i < a.length; i++) { int cur = Math.max(prev1, Math.max(prev2 + a[i], a[i]));Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Forgetting the start-fresh term
On [-5, 10], taking 10 atop dp[i-2] = -5 gives 5, but 10 alone is better. Without the a[i] term you'd report 5 instead of 10.
Explore the code comparisonHide the code comparison
Forgetting the start-fresh term
On [-5, 10], taking 10 atop dp[i-2] = -5 gives 5, but 10 alone is better. Without the a[i] term you'd report 5 instead of 10.
Don't write this
cur = max(prev1, prev2 + a[i])Write this instead
cur = max(prev1, prev2 + a[i], a[i])2Taking on dp[i-1] instead of dp[i-2]
Adding a[i] to dp[i-1] lets index i pair with index i-1 — exactly the adjacency the problem forbids.
Explore the code comparisonHide the code comparison
Taking on dp[i-1] instead of dp[i-2]
Adding a[i] to dp[i-1] lets index i pair with index i-1 — exactly the adjacency the problem forbids.
Don't write this
int cur = Math.max(prev1, prev1 + a[i]); // prev1 = dp[i-1]: adjacency allowedWrite this instead
int cur = Math.max(prev1, Math.max(prev2 + a[i], a[i])); // prev2 = dp[i-2]3Top-down recursion in Python at scale
Python's default recursion limit (~1000) overflows at n = 10⁵. Tabulation uses a loop and has no such limit.
Explore the code comparisonHide the code comparison
Top-down recursion in Python at scale
Python's default recursion limit (~1000) overflows at n = 10⁵. Tabulation uses a loop and has no such limit.
Don't write this
def f(i):
if i >= len(a): return 0
return max(a[i] + f(i + 2), f(i + 1)) # RecursionError for large nWrite this instead
for i in range(2, n):
dp[i] = max(dp[i - 1], dp[i - 2] + a[i], a[i])Revise
Carry the pattern into the interview
Pattern to remember
Non-adjacent selection DP (House Robber family)
Code pattern to memorize
dp[i] = max(dp[i-1], dp[i-2] + a[i], a[i])
prev2, prev1 = 0, a[0]
for i in 1..n-1:
cur = max(prev1, prev2 + a[i], a[i])
prev2, prev1 = prev1, cur
return prev1Rules to remember
- 1At each index you have exactly two choices: take it (then skip the neighbour) or skip it. That binary choice is the DP.
- 2The third term
a[i](start fresh) matters because negatives indp[i-2]can make taking the element alone better than adding it to a worse prefix. - 3Only the last two dp values feed the next one → collapse the array to two rolling variables for O(1) space.
- 4Adjacent means index
iandi-1; skipping more than one in a row is always allowed. - 5Initialise
prev2 = 0sodp[i-2] + a[i]ati = 1correctly reduces to justa[i].
Use this pattern when
- Choose a subset maximising/minimising a value with a 'no two adjacent' constraint
- Linear sequence where taking one item forbids its immediate neighbour
- House Robber / scheduling-with-cooldown shaped problems
- You can skip arbitrarily many elements (unlike fixed-window problems)
Choose another pattern when
- Adjacency wraps around a circle → House Robber II (run the linear DP twice)
- Contiguous subarray required, not a subset → Kadane's algorithm
- No adjacency constraint at all → just sum the positives
- 2D grid adjacency → grid DP, not this 1D recurrence
Interview takeaway
When taking an element forbids its neighbour, the answer at each position depends only on the answers two and one steps back. Recognise that and you've solved the whole House Robber family — including this skipping/negative variant — with two variables.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternCanonical non-adjacent selection DP (House Robber family). Whenever choosing an element forbids its neighbour, the state two steps back plus one step back determines the answer — collapse to two scalars for O(1) space. Same shape as House Robber, Delete and Earn, and cooldown-stock problems.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- House RobberPositives only — the same recurrence without needing the start-fresh term.
- House Robber IICircular array — run the linear DP twice, once excluding the first house and once the last.
- Delete and EarnComing soonBucket values into counts, then it's House Robber on the value axis.
- Maximum SubarrayComing soonContiguous (not subset) — Kadane's, the contrast case for the anti-signal.