DAY 25 / 30 · WEEK 4

Dynamic Programming I

Goal of the day

Concept

Imagine grading homework where the same sub-question ("what's 7×8?") shows up inside three different problems. If you re-derive 7×8 from scratch every single time you see it, you're wasting work — you already know the answer, you just forgot you'd already solved it. If instead you write "7×8 = 56" on a sticky note the first time, every later occurrence is a free lookup. That sticky note is the entire idea behind dynamic programming (DP): a problem has overlapping subproblems when the same smaller question recurs, and it has optimal substructure when the big answer can be assembled directly from those smaller answers (no need to reconsider choices already made). When both hold, don't recompute — cache.

Day 9 built naive recursive fib(n) and watched its call tree explode: every call branches into two more calls, and — this is the important part — many of those calls ask the exact same question. fib(4) calls fib(2) twice, from two different branches, each time re-deriving it completely from scratch. That's wasted, overlapping work, and it's exactly what DP is for.

There are two equivalent ways to apply the sticky note:

Same idea, opposite direction: memoization starts at the big question and works its way down to base cases as needed; tabulation starts at the base cases and works its way up to the big question unconditionally.

Diagram: naive recursion (Day 9) vs. memoized — same fib(4) call

NAIVE — Day 9's fib(4), no cache (9 calls total) fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(0) fib(1) fib(0) Both amber fib(2) boxes are the SAME subproblem, computed from scratch twice — that's an overlapping subproblem. fib(1) recurs 3 times, fib(0) recurs 2 times. MEMOIZED — same fib(4) call, with a cache (7 calls, 1 is a cache hit) fib(4) fib(3) fib(2) ✓ fib(2) fib(1) fib(1) fib(0) cache hit — no recursion The 2nd fib(2) call reuses the cache instead of expanding 2 more calls.

Diagram: filling a DP table left to right

Climbing Stairs DP table (n=8) — dp[i] = dp[i-1] + dp[i-2] dp[1]dp[2]dp[3] dp[4]dp[5]dp[6] dp[7]dp[8] 1 2 3 5 8 ? · · Green = filled and confirmed. Purple = computing now, pulling from the 2 boxes it points to. Gray = not reached yet. dp[6] = dp[5] + dp[4] = 8 + 5 = 13.

Interactive visualization: filling the DP table

Same recurrence, two differently-worded problems — watch both tables fill cell by cell.

filled / confirmed source cell being read cell being computed now

1. Fibonacci — bottom-up table

Press Play or Step to begin.

2. Climbing Stairs — the same recurrence, a different name

Press Play or Step to begin.

The code

// 1. NAIVE recursion (Day 9's version) — O(2^n), recomputes overlapping
//    subproblems from scratch every time.
function fibNaive(n) {
  if (n <= 1) return n;                          // base cases: fib(0)=0, fib(1)=1
  return fibNaive(n - 1) + fibNaive(n - 2);        // recursive case
}

// 2. MEMOIZED (top-down) — same recursion, but a cache means every
//    distinct subproblem is only ever solved once. O(n) time.
function fibMemo(n, memo = {}) {
  if (n in memo) return memo[n];                   // cache hit — return instantly
  if (n <= 1) return n;                            // base cases
  memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
  return memo[n];
}

// 3. TABULATED (bottom-up) — no recursion at all, fill an array in order.
//    O(n) time, O(n) space for the table.
function fibTab(n) {
  if (n <= 1) return n;
  const dp = new Array(n + 1);
  dp[0] = 0;
  dp[1] = 1;
  for (let i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
  return dp[n];
}

// 4. TABULATED with O(1) space — dp[i] only ever needs the LAST TWO
//    values, so keep two rolling variables instead of a whole array.
function fibOptimal(n) {
  if (n <= 1) return n;
  let prev2 = 0, prev1 = 1;                        // dp[0], dp[1]
  for (let i = 2; i <= n; i++) {
    const cur = prev1 + prev2;                     // dp[i] = dp[i-1] + dp[i-2]
    prev2 = prev1;                                 // slide the window forward
    prev1 = cur;
  }
  return prev1;
}

// Climbing Stairs (LC 70) — structurally identical recurrence to fibonacci,
// just different base cases and a 1-indexed problem statement.
function climbStairs(n) {
  if (n <= 2) return n;                            // ways(1)=1, ways(2)=2
  let prev2 = 1, prev1 = 2;
  for (let i = 3; i <= n; i++) {
    const cur = prev1 + prev2;                     // ways(i) = ways(i-1) + ways(i-2)
    prev2 = prev1;
    prev1 = cur;
  }
  return prev1;
}

Line by line:

Dry run: climbStairs(8) — bottom-up table fill

idp[i-1]dp[i-2]dp[i] = dp[i-1] + dp[i-2]
1 (base)dp[1] = 1 (one way: a single step)
2 (base)dp[2] = 2 (two ways: 1+1, or 2)
3dp[2] = 2dp[1] = 1dp[3] = 2 + 1 = 3
4dp[3] = 3dp[2] = 2dp[4] = 3 + 2 = 5
5dp[4] = 5dp[3] = 3dp[5] = 5 + 3 = 8
6dp[5] = 8dp[4] = 5dp[6] = 8 + 5 = 13
7dp[6] = 13dp[5] = 8dp[7] = 13 + 8 = 21
8dp[7] = 21dp[6] = 13dp[8] = 21 + 13 = 34

Final answer: 34 distinct ways to climb 8 stairs, taking 1 or 2 steps at a time. Each row only ever reads the two rows directly above it — never anything earlier, and never anything recomputed.

Complexity

ApproachTimeSpaceWhy
Naive recursiveO(2^n)O(n)Every call branches into 2 more calls with no cache, so the call count roughly doubles per level of depth. Space is O(n) for the call stack (deepest chain of waiting frames), not the exponential call count itself.
Memoized (top-down)O(n)O(n)Each of the n distinct subproblems (fib(0) through fib(n)) is computed exactly once and then cached; every later call for the same n is an O(1) lookup. Space is the cache (n entries) plus the O(n) call stack.
Tabulated (bottom-up)O(n)O(n)A single loop from 0 to n, one O(1) step per cell — no recursion, no call stack, but the full table is kept, so space is still O(n).
Tabulated, rolling variablesO(n)O(1)Identical loop, but only the last 2 values are ever read, so 2 variables replace the whole array — the same optimization applied to climbStairs above.

The blowup is real, not theoretical: for fib(10), the naive version makes 177 calls where the memoized version makes 19. For fib(30), that gap widens to 2,692,537 naive calls vs. just 59 memoized calls — verified by literally counting recursive calls, not just citing Big-O.

Interview corner

How to talk through it out loud: name the recurrence before writing any code — "dp[i] depends on dp[i-1] and dp[i-2], so this is the fibonacci-shaped recurrence" — then state the base cases explicitly, then say whether you're going top-down or bottom-up and why. Interviewers are listening for whether you can identify overlapping subproblems and optimal substructure on sight, not just whether the final code happens to work.

Practice problems

1. Climbing Stairs (Easy — LeetCode: Climbing Stairs)

You're climbing a staircase with n steps. Each move you can climb 1 or 2 steps. Return the number of distinct ways to reach the top. Today's second worked example — the code above is the real solution.

2. Min Cost Climbing Stairs (Easy — LeetCode: Min Cost Climbing Stairs)

Each stair i has a cost cost[i] to step on it. You may start from step 0 or step 1, and from a step you can climb 1 or 2 steps. Return the minimum total cost to reach the top (one step past the last index).

3. Unique Paths (Medium — LeetCode: Unique Paths)

A robot sits at the top-left corner of an m x n grid and can only move right or down. Return the number of distinct paths to the bottom-right corner. Same idea as today's 1D recurrences, stretched to 2 dimensions — a preview of the grid DP shapes coming up on Day 26.

Quiz

1. What two properties must a problem have for dynamic programming to apply?

2. What's the one core change memoization makes to naive recursion?

3. What's the time complexity of naive recursive fib(n), with no memoization?

4. When can a tabulated DP be optimized from O(n) space down to O(1) with rolling variables?

5. Why is Climbing Stairs considered "the same DP" as fibonacci?