3.7.8 · D4Algorithm Paradigms

Exercises — Tabulation (bottom-up DP) — iterative

2,502 words11 min readBack to topic

Level 1 — Recognition

You should be able to spot the pieces (state, base, order, answer) and read a filled table.


L1.1 — Read the table

Given the Fibonacci table below, what is dp[6]? Recall the rule is dp[i]=dp[i-1]+dp[i-2].

i 0 1 2 3 4 5 6
dp[i] 0 1 1 2 3 5 ?
Recall Solution

The rule says each box = the two boxes just before it. WHAT we do: add dp[5] and dp[4]. WHY: the recurrence dp[6]=dp[5]+dp[4] — both are already filled, so we may read them.


L1.2 — Name the direction

A student writes: "I start at dp[n] and recurse downward, caching results." Is this tabulation or memoization?

Recall Solution

Starting big and recursing down = memoization (top-down). See Memoization (top-down DP). Tabulation is the opposite: start at the smallest boxes and loop upward. Both compute each subproblem once; only the travel direction differs.


L1.3 — Which cell holds the answer?

For Climbing Stairs (dp[i] = number of ways to reach step i) with n = 4, which single cell is the final answer, and what fills the base cases?

Recall Solution

Answer cell: the largest state, dp[4]. Base cases: dp[0]=1 (one way — stand still) and dp[1]=1 (one way — a single 1-step). Filling upward: dp[2]=2, dp[3]=3, dp[4]=5.


Level 2 — Application

Now you build small tables from scratch by applying the recipe.


L2.1 — Fibonacci table for n = 7

Fill the whole table and report dp[7].

Recall Solution

Seed: dp[0]=0, dp[1]=1. Loop small→large, each box = sum of previous two:

i 0 1 2 3 4 5 6 7
dp[i] 0 1 1 2 3 5 8 13


L2.2 — Coin Change (counting ways)

Coins {1, 2}, target amount 5. dp[a] = number of ways to make amount a (order does not matter). See Coin Change. The standard tabulation loops coins on the outside, amounts inside: Fill the table and report dp[5].

Recall Solution

State: dp[a] = number of combinations summing to a. Base: dp[0]=1 (one way to make 0 — pick nothing). WHY coins outside: to count combinations (not permutations), we must commit to one coin denomination fully before the next, so {1,2} and {2,1} are not double-counted.

Start dp = [1,0,0,0,0,0].

Process coin 1 (each amount gains ways from a-1): dp = [1,1,1,1,1,1]. Process coin 2 (each a≥2 gains from a-2):

  • dp[2]+=dp[0]=1 → 2
  • dp[3]+=dp[1]=1 → 2
  • dp[4]+=dp[2]=2 → 3
  • dp[5]+=dp[3]=2 → 3

Final dp = [1,1,2,2,3,3], so . The three ways: 1+1+1+1+1, 1+1+1+2, 1+2+2. ✅


L2.3 — Minimum steps recurrence

dp[i] = fewest coins to make amount i with coins {1,3,4}. Give the recurrence, then compute dp[6].

Recall Solution

Recurrence: try each coin as the last one used: WHY min, not sum: here we optimize a quantity (fewest coins), so we pick the best last choice — contrast with L2.2 where we count and therefore add.

Build upward:

i 0 1 2 3 4 5 6
dp[i] 0 1 2 1 1 2 2

Check dp[6]: candidates 1+dp[5]=3, 1+dp[3]=2, 1+dp[2]=3. Minimum is (i.e. 3+3).


Level 3 — Analysis

Reason about order, dependencies, and why a table cell equals what it does.


L3.1 — Trace the 0/1 Knapsack grid

Items (w,v) = (1,1), (3,4), (4,5), capacity W=4. Using fill the grid and report dp[3][4]. See 0-1 Knapsack.

Figure — Tabulation (bottom-up DP) — iterative
Recall Solution

Rows = items considered (0 = none); columns = capacity c from 0 to 4. Row 0 is all zeros (no items → no value).

Row 1 — item (1,1): for c≥1 we may take it. dp[1][c]=max(0, 1+dp[0][c-1])=1. [0, 1, 1, 1, 1]

Row 2 — item (3,4): takeable when c≥3.

  • c=0,1,2: too heavy → copy above → 0,1,1
  • c=3: max(dp[1][3]=1, 4+dp[1][0]=4)=4
  • c=4: max(dp[1][4]=1, 4+dp[1][1]=5)=5 [0, 1, 1, 4, 5]

Row 3 — item (4,5): takeable only at c=4.

  • c=0..3: copy above → 0,1,1,4
  • c=4: max(dp[2][4]=5, 5+dp[2][0]=5)=5 [0, 1, 1, 4, 5]

Two optimal packings tie at value 5: item 3 alone, or items 1+2 (1+4).


L3.2 — Why can't we loop rows large→small?

In the knapsack loop for i in 1..n, could we instead loop i from n down to 1? Explain what breaks.

Recall Solution

Every entry dp[i][c] reads row i-1. If we fill row n first, row n-1 is still all zeros → we read garbage. WHAT breaks: dependencies are read before they exist. The rule: iterate so each cell's inputs are already filled. Since dp[i] depends on dp[i-1], we must go small i → large i. The order is dictated by the arrows in the recurrence, never by taste.


L3.3 — Space-optimized 1D knapsack direction

We collapse the 2D grid to one row dp[0..W] reused per item. Should we loop capacity c high→low or low→high? Prove your choice with a tiny counterexample using item (2,3), W=4.

Recall Solution

Correct: high→low. See Space Optimization in DP.

The 1D update is dp[c] = max(dp[c], v + dp[c-w]), where dp[c-w] must mean the previous row (item not yet used).

Low→high (WRONG): start dp=[0,0,0,0,0], item (2,3).

  • c=2: dp[2]=max(0,3+dp[0])=3
  • c=4: dp[4]=max(0,3+dp[2])=3+3=6 ← used the item twice! (unbounded, not 0/1)

High→low (RIGHT):

  • c=4: dp[4]=max(0,3+dp[2]=3+0)=3
  • c=2: dp[2]=max(0,3+dp[0])=3
  • final dp=[0,0,3,0,3] → best value 3, item used once. ✅

Going high→low guarantees dp[c-w] was not yet overwritten this round, so it still holds the previous row.


Level 4 — Synthesis

Design a table for a new problem by combining recipe steps yourself.


L4.1 — Longest Common Subsequence

Strings A = "ABCBDAB", B = "BDCAB". Define dp[i][j] = length of the LCS of the first i letters of A and first j letters of B. State the recurrence, then compute dp[7][5]. See Longest Common Subsequence.

Figure — Tabulation (bottom-up DP) — iterative
Recall Solution

State: dp[i][j] = LCS length of A[0..i) and B[0..j). Base: dp[0][j]=dp[i][0]=0 (empty string shares nothing). Recurrence: WHY: if the last letters match, they can end a common subsequence, so we take the LCS of everything before them and add 1. If not, at least one of the two last letters is unused → try dropping each and keep the larger.

Filling the grid (rows = letters of A, cols = letters of B), the final cell is: One such LCS of length 4 is "BCAB" (or "BDAB").


L4.2 — Choose the state yourself

"Given costs [2,7,9,3,1] on houses in a row, pick a subset with no two adjacent houses, maximising total. n=5." Define dp, give the recurrence and answer.

Recall Solution

State: dp[i] = best total using houses 0..i under the no-two-adjacent rule. Recurrence: for house i, either skip it (dp[i-1]) or take it (cost[i] + dp[i-2], since i-1 is now forbidden): Base: dp[0]=cost[0]=2, dp[1]=max(cost[0],cost[1])=7.

i 0 1 2 3 4
cost 2 7 9 3 1
dp 2 7 11 11 12
  • dp[2]=max(7, 9+2)=11
  • dp[3]=max(11, 3+7)=11
  • dp[4]=max(11, 1+11)=12


Level 5 — Mastery

Prove, generalise, or catch a subtle bug.


L5.1 — Base-case bug hunt

A student's Coin Change (min coins) returns wrong answers. Their code initialises dp = [0]*(amount+1). What is the bug and the fix?

Recall Solution

Bug: they initialised every cell to 0, but only dp[0] should be 0. Every other cell must start at "impossible" = a large sentinel (e.g. amount+1 or float('inf')). Why it feels right: 0 is the natural empty value and matches the true dp[0]. What breaks: the recurrence dp[i]=1+min(dp[i-coin]) reads other cells; if they are 0, the min wrongly believes any amount is reachable with almost no coins, poisoning everything. Fix: A cell still equal to at the end means "amount unreachable". Base cases seed the table; a wrong seed is as fatal as a missing one.


L5.2 — Prove no recomputation

Argue why tabulation computes each subproblem exactly once, unlike naive recursion.

Recall Solution

Naive recursion re-enters the same subproblem through different call paths (the Overlapping Subproblems property), so F(n) can spawn calls. Tabulation visits each cell in a single loop pass. Formally: the outer loop index takes each value in its range once; each cell is written exactly once (in its loop iteration) and thereafter only read. Because we order the loop so dependencies precede the cell (Optimal Substructure guarantees dependencies are smaller states), no cell is ever revisited to compute it. Result: total work = (number of cells) × (cost per cell). For Fibonacci that is vs the exponential recursion.


L5.3 — Generalise the loop direction rule

State a single rule that decides safe iteration direction for any in-place DP, covering both Fibonacci (any order safe when using two vars) and 1D knapsack (must go high→low).

Recall Solution

The rule: Iterate so that every cell you read during an update still holds the value from the intended prior stage — not a value already overwritten this pass.

  • If reads reference strictly earlier, never-overwritten cells (Fibonacci with rolling vars) → direction is free.
  • If a read dp[c-w] and the write dp[c] live in the same array being overwritten this round, choose the direction that visits the read location after the write location, so the read still sees the old value. For knapsack, c-w < c, so writing high→low keeps dp[c-w] untouched until later. ✅

This is exactly the Space Optimization in DP safety condition, and it subsumes every mistake in the parent note.


Recall Feynman recap

Every problem here was the same four moves: name the box (state), write the fill rule (recurrence), hand-fill the boxes no rule reaches (base), and loop so a box's ingredients are ready before you cook it (order). Counting problems add; best/least problems take min/max; in-place tricks live or die by loop direction. That's the whole ladder.

Connections