DAY 26 / 30 · WEEK 4

Dynamic Programming II

Goal of the day

Concept

Day 25's recurrence never had to make a decision — dp[i] = dp[i-1] + dp[i-2] just adds two numbers together, every time, no matter what. Today's two problems both break that pattern in different ways, and recognizing which way a new problem breaks it is the actual interview skill.

House Robber, as a decision. Imagine walking down a street of houses, where robbing two houses next to each other trips the alarm. At each house you face exactly one choice: skip it and carry forward whatever you'd already secured up to the previous house, or rob it and add its value to whatever you'd secured two houses back (since the house right before it is now off-limits). You take whichever of those two options is worth more — that's dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Notice the shape: it's a max of two options, not a sum of two cells. Day 25's recurrence always uses both previous cells together; this one uses exactly one of them, whichever wins.

Coin Change, as an unbounded search. Now imagine making change for an amount using a fixed set of coin denominations, where you can use as many of any coin as you like — a handful of $1 coins is just as legal as a single $5. To find the minimum coins for amount amt, try every coin denomination that's small enough to fit: for a given coin c, if you use one of it, you still need to make amt - c with whatever's optimal there, plus this one coin. Take the cheapest option over every coin: dp[amt] = min(dp[amt - c] + 1) for every coin c ≤ amt. The word "unbounded" refers to the coins, not the amount — each coin can be reused without limit, which is exactly why the recurrence has to check every coin at every cell instead of reading a fixed two cells back like fibonacci or House Robber.

Lined up side by side, three structurally different recurrences:

Same "sticky note" idea as Day 25 underneath all three — don't recompute a subproblem you've already solved — but the shape of what gets written on each sticky note, and how many earlier notes get read to fill in a new one, is different every time.

Diagram: House Robber — one cell's decision (max, not sum)

House Robber — one cell's decision: dp[i] = max(dp[i-1], dp[i-2] + nums[i]) Worked example: nums = [2, 7, 9, 3, 1], computing dp[2] (i = 2) rob: dp[0] + nums[2] = 2 + 9 = 11 skip: dp[1] = 7 2 7 ? dp[0] dp[1] dp[2] Blue borders = source cells being read. Purple = the cell being computed now. nums[2] = 9, dp[1] = 7, dp[0] = 2. dp[2] = max(dp[1], dp[0]+nums[2]) = max(7, 2+9) = max(7, 11) = 11 → rob wins.

Diagram: Coin Change — the unbounded inner loop

Coin Change — inner loop over every coin, then take the min Worked example: coins = [1, 2, 5], amount = 11 — computing dp[6] dp[5]=1 coin 1 → dp[5] + 1 = 1 + 1 = 2 dp[4]=2 coin 2 → dp[4] + 1 = 2 + 1 = 3 dp[1]=1 coin 5 → dp[1] + 1 = 1 + 1 = 2 dp[6] = min(2, 3, 2) = 2 — cheapest of the 3 candidates wins.

Interactive visualization: filling the DP table

Two different recurrence shapes, same left-to-right table fill — watch what each cell reads and how many earlier cells it has to check.

filled / confirmed source cell being read cell being computed now

1. House Robber — max(skip, rob) at every cell

Press Play or Step to begin.

2. Coin Change — the inner loop over every coin

Press Play or Step to begin.

The code

// 1. HOUSE ROBBER (LC 198) — O(1) space, two rolling variables.
//    prev2 stands in for "dp[i-2]", prev1 for "dp[i-1]", both seeded at 0
//    (robbing nothing before house 0 is worth 0 — the same trick as an
//    empty base case).
function houseRobber(nums) {
  let prev2 = 0, prev1 = 0;
  for (let i = 0; i < nums.length; i++) {
    const cur = Math.max(prev1, prev2 + nums[i]);  // skip vs. rob
    prev2 = prev1;                                  // slide the window forward
    prev1 = cur;
  }
  return prev1;
}

// 2. COIN CHANGE (LC 322) — 1D tabulated array, Infinity sentinel.
//    dp[amt] = fewest coins to make amt; dp[0] = 0 (base case); every
//    other slot starts at Infinity, meaning "not reachable yet."
function coinChange(coins, amount) {
  const dp = new Array(amount + 1).fill(Infinity);
  dp[0] = 0;
  for (let amt = 1; amt <= amount; amt++) {
    for (const coin of coins) {                     // try EVERY coin — unbounded reuse
      if (coin <= amt && dp[amt - coin] + 1 < dp[amt]) {
        dp[amt] = dp[amt - coin] + 1;
      }
    }
  }
  return dp[amount] === Infinity ? -1 : dp[amount];  // never reached — no combination works
}

Line by line:

Dry run: houseRobber([2, 7, 9, 3, 1]) — rolling max

inums[i]prev1 (before)prev2 (before)cur = max(prev1, prev2+nums[i])
0200max(0, 0+2) = 2
1720max(2, 0+7) = 7
2972max(7, 2+9) = 11
33117max(11, 7+3) = 11
411111max(11, 11+1) = 12

Final answer: 12. Trace which branch of the max won at each step to recover the actual houses: dp[4] = 12 came from the "rob" branch (dp[2] + nums[4] = 11 + 1), so house 4 is robbed; dp[2] = 11 also came from its "rob" branch (dp[0] + nums[2] = 2 + 9), so house 2 is robbed too; dp[0] is a base case, so house 0 is robbed. The optimal set is houses at indices 0, 2, and 4 — values 2, 9, and 1 — which sums to 12, with no two adjacent.

Dry run: coinChange([1, 2, 5], 11) — bottom-up table

amtbest coinfrom dp[amt-coin] + 1dp[amt]
0base case0
11dp[0]+1 = 11
22dp[0]+1 = 11
31 or 2dp[1]+1 = 2, dp[2]+1 = 22
42dp[2]+1 = 22
55dp[0]+1 = 11
61 or 5dp[5]+1 = 2, dp[1]+1 = 22
72 or 5dp[5]+1 = 2, dp[2]+1 = 22
81, 2, or 5dp[7]+1=3, dp[6]+1=3, dp[3]+1=33
92 or 5dp[7]+1=3, dp[4]+1=33
105dp[5]+1 = 22
111 or 5dp[10]+1=3, dp[6]+1=33

Final answer: dp[11] = 3 — five plus five plus one (5 + 5 + 1 = 11, 3 coins). Note dp[10] = 2 (two fives) reuses coin 5 twice — that's the "unbounded" part in action.

Complexity

ProblemTimeSpaceWhy
House Robber (optimal)O(n)O(1)A single pass over the houses, and every cell does a fixed, constant amount of work (one comparison, one max) — same shape as Day 25's rolling-variable fibonacci, just with a max instead of a sum.
Coin Change (tabulated)O(amount × coins)O(amount)The outer loop runs once per amount from 1 to the target (O(amount) iterations); the inner loop tries every coin denomination at each of those amounts (O(coins) per iteration). Nested, that's O(amount × coins) total — the first DP in the course whose per-cell cost isn't O(1). Space is the dp array itself, one slot per amount from 0 to the target.

This is the key complexity distinction of the day: House Robber's inner work per cell is constant (just compare 2 numbers), so its total time is linear in the input size. Coin Change's inner work per cell scales with the number of coin denominations, so its total time is the product of two input sizes, not a sum of one.

Interview corner

How to talk through it out loud: name the recurrence shape before writing code — "this is a max over 2 fixed options" (House Robber) or "this is a min over every coin, so I need a nested loop" (Coin Change) — then state the base case and the sentinel value you're seeding the table with, and why (0 for "free," Infinity for "not yet reachable"). Interviewers are listening for whether you can name the shape of the recurrence and justify your sentinel choice before the code even runs, not just whether the final answer happens to be correct.

Practice problems

1. House Robber (Easy — LeetCode: House Robber)

Given an array of non-negative integers representing money stashed in each house along a street, return the maximum amount you can rob without robbing two adjacent houses. Today's first worked example — the code above is the real solution.

2. Coin Change (Medium — LeetCode: Coin Change)

Given coin denominations and a target amount, return the fewest number of coins needed to make that amount, or -1 if it's not possible with any combination. Today's second worked example — the code above is the real solution.

3. Longest Increasing Subsequence (Medium — LeetCode: Longest Increasing Subsequence)

Given an integer array, return the length of the longest strictly increasing subsequence (elements don't need to be contiguous, just in increasing order and in their original relative order). A new, structurally different DP shape — each cell has to look back at every earlier index, not just a fixed 2.

Quiz

1. Why is House Robber's recurrence dp[i] = max(dp[i-1], dp[i-2]+nums[i]) instead of a sum like Day 25's fibonacci?

2. Why does Coin Change need an inner loop over every coin at each amount, instead of a fixed lookback like fibonacci?

3. Why does Coin Change seed its dp array with Infinity instead of 0, and check for Infinity at the end?

4. What's the recurrence shape for Longest Increasing Subsequence's dp[i]?

5. What's the time complexity of the tabulated Coin Change solution?