latency.lab
Algorithms · Topic 12

1-D Dynamic Programming

Level: foundationTool: recognizing and caching repeated subproblemsTime: ~75 min

By the end you can

Dynamic programming is not a data structure or a traversal order — it's a discipline: notice that a brute-force recursive solution keeps solving the exact same smaller subproblem over and over, then remember the answer the first time instead of re-deriving it. Everything else in this topic (which array index to use as the table, what order to fill it in) is detail in service of that one idea.

From exponential recursion to a table

Climbing Stairs is the cleanest possible version of this. The natural recursive definition — ways(n) = ways(n-1) + ways(n-2) — is correct and also catastrophic: computing ways(n-1) recomputes ways(n-2) from scratch, which the outer call is also about to compute directly. That duplication compounds every level down, and ways(40) alone makes on the order of 2^40 calls. The fix isn't a smarter recursion — it's building the answer bottom-up in a table (or, when only the last one or two entries are ever needed, in a couple of rolling variables), so each subproblem gets computed exactly once.

The habit to build

Before writing any code, answer two questions: what does dp[i] actually mean in plain English (not just "the answer for i"), and what earlier entries does computing it depend on? If you can't state the meaning in one sentence, the recurrence that follows will be guesswork.

1
2
3
5
8
dp[1]
dp[2]
dp[3]
dp[4]
dp[5]
Climbing Stairs: dp[5] (dashed, being computed) only ever needs dp[4] and dp[3] (teal, already computed) — it never re-derives anything below them. Each entry is computed exactly once, the whole reason this is O(n) instead of the exponential naive recursion.

Take it or skip it: House Robber's shape

House Robber is the shape behind more problems than its premise suggests: at each position, you make one binary choice — skip it and carry forward the best total so far, or take it and add its value to the best total from *two* steps back (since taking it forbids its neighbor). That single recurrence, best(i) = max(best(i-1), best(i-2) + nums[i]), is the same "take or skip" shape that shows up again in Partition Equal Subset Sum, just with "adjacent" replaced by "already used." House Robber II's circular twist doesn't need new machinery at all — it needs the *insight* that a circular constraint just means solving the linear version twice (excluding one end each time) and taking the better answer.

Unbounded reuse: Coin Change's shape

Coin Change and Perfect Squares share a different shape: at each amount, try *every* choice (every coin, every perfect square) and reuse is unlimited, so the recursive step reaches into dp[amount - choice] for every choice available — not just the immediately preceding index. Coin Change II adds one more subtlety on top: whether you're counting *combinations* (order doesn't matter) or *sequences* (order matters) is entirely determined by which loop is on the outside — coins outer, amount inner, counts each combination once; swap them and you're counting permutations instead, a completely different (larger) number for the exact same recurrence.

loop-order-changes-the-question.cpp
// Coin outer, amount inner: counts COMBINATIONS (order doesn't matter)
for (int coin : coins)
    for (int a = coin; a <= amount; ++a)
        dp[a] += dp[a - coin];

// Swapped: counts every ORDERING of the same coins separately -- a
// different (larger) number, even though the recurrence LOOKS identical.

Used-at-most-once: Partition Equal Subset Sum's direction trap

Partition Equal Subset Sum reduces to subset-sum: is some subset achievable summing to half the total? The 0/1 knapsack table for this has the same easy-to-miss trap in the *opposite* direction from Coin Change's loop-order issue: the achievable-sum range must be iterated downward for each number, so a number never combines with a sum that was updated using that same number earlier in the same pass. Iterate upward by mistake, and a single number quietly becomes reusable — the unbounded-knapsack answer, not the 0/1 one.

Two running values instead of one: Maximum Product Subarray

Maximum Subarray (Kadane's algorithm) tracks one running value — the best sum ending here. Maximum Product Subarray looks identical at first glance, but multiplication has a trap addition doesn't: a negative number can flip the sign of everything that follows, turning the current running *minimum* (a large-magnitude negative product) into the new *maximum*. The fix is tracking both a running max and a running min at every step, and computing the next max/min from *both* previous values — not just the previous max.

Common mistakes

  • Reaching for recursion without a memo table — the recurrence itself is usually the easy part; forgetting to cache repeated subproblems is what turns a correct O(n) or O(n²) idea into an exponential one.
  • Getting Coin Change II's loop order backwards — amount-outer, coin-inner silently counts permutations instead of combinations, with no crash or obviously wrong-looking output to flag it.
  • Iterating the wrong direction in a 0/1 knapsack table — upward iteration silently allows reuse of an item that should only be usable once.
  • Tracking only a running max for a product-based recurrence — Maximum Product Subarray needs the running min too, since a negative multiplier can make the current worst value become the new best.

Takeaways

  • DP is "notice repeated subproblems, then cache them" — state what dp[i] means in plain English before writing the recurrence.
  • "Take it or skip it" (House Robber) and "try every choice, reuse unlimited" (Coin Change) are two distinct recurrence shapes — recognizing which one a problem matches is most of the work.
  • Loop order and iteration direction can silently change *which question* a 0/1-knapsack-shaped table is actually answering, with no obvious symptom besides a wrong number.
  • A recurrence that looks like Kadane's may still need more than one running value — check whether a sign flip (or similar contrast) can turn the current worst case into the next best case.

Try it: the foundational recurrence

Climbing Stairs is the cleanest version of "cache the subproblem instead of re-deriving it" — get this automatic here, since every other problem in this topic builds on the same discipline.

Live

Loading starter code…

Try it: take it or skip it

House Robber — the two-rolling-variables version of this recurrence shape shows up again in several later problems.

Live

Loading starter code…

Try it: unlimited reuse

Coin Change — the other core recurrence shape in this topic, and the one Perfect Squares reuses directly.

Live

Loading starter code…

Keep going

That's 3 of the 12 problems in this topic. Coin Change II is worth doing right after Coin Change — the loop-order subtlety there is one of the easiest silent bugs in this entire course to write by accident. See the full 1-D Dynamic Programming set →

Checkpoint · 1-D Dynamic Programming

5 questions · pass at 70%

Q01
Climbing Stairs' recurrence ways(n) = ways(n-1) + ways(n-2) is correct as plain recursion, but ways(40) alone makes on the order of 2^40 calls without memoization. Where does that blowup actually come from?
WhyThe recurrence is correct -- the problem is that naive recursion re-derives identical subproblems many times over, with the duplication multiplying at every level. A bottom-up table (or two rolling variables) computes each of the n subproblems exactly once instead.
Q02
House Robber II handles a circular arrangement by solving House Robber (the linear version) twice -- once excluding the last house, once excluding the first -- and taking the better result. Why does this actually cover every valid circular selection?
WhySince the first and last houses are adjacent in a circle, no valid selection can rob both. That means every valid circular selection is ALSO a valid selection for at least one of the two linear sub-problems (excluding the first house, or excluding the last) -- so checking both and taking the max is guaranteed to find the true optimum.
Q03
Coin Change II counts combinations (order doesn't matter) by putting coins on the OUTER loop and amount on the INNER loop. What specifically changes if those two loops are swapped?
WhyCoins-outer, amount-inner processes one coin denomination fully before considering the next, which is exactly what prevents the same set of coins from being counted once per ordering. Amount-outer, coin-inner instead lets earlier coin choices at a given amount interact with later ones in a way that distinguishes orderings -- counting sequences, not combinations.
Q04
Partition Equal Subset Sum's 0/1 knapsack table iterates the achievable-sum range DOWNWARD for each number, from target down to that number's value. What goes wrong if it iterates upward instead?
WhyThe whole point of the downward iteration is to guarantee that when dp[s] is updated using x, the dp[s-x] it reads reflects state from BEFORE x was considered this round -- enforcing at-most-once use. Iterating upward breaks that guarantee, letting a number quietly combine with a sum that already used it, which is a silent correctness bug, not a crash.
Q05
Maximum Product Subarray tracks both a running MAX and a running MIN ending at each index, unlike Maximum Subarray (Kadane's), which only needs a running max. Why is the running min necessary here specifically?
WhyAddition never reverses order (adding a negative number just makes things smaller), but multiplication by a negative number flips the sign -- so the most negative product so far can become the LARGEST product after multiplying by another negative. Only tracking a running max would silently miss exactly this case.

Finished 1-D Dynamic Programming?

Pass the quiz to complete it automatically.