Exercises — Dynamic programming — overlapping subproblems, optimal substructure
Level 1 — Recognition
Goal: decide whether DP even applies, using the two-property test "OO" (Overlapping + Optimal substructure).
L1.1
For each problem, say whether it has overlapping subproblems, optimal substructure, both, or neither: (a) Fibonacci, (b) Merge Sort, (c) finding the maximum of an unsorted array by scanning.
Recall Solution
(a) Fibonacci — both. The call tree for recomputes for small enormously many times → overlap. And literally builds the answer from sub-answers → optimal substructure. ✅ DP applies. (b) Merge Sort — optimal substructure only. Sorting the whole is built from sorting each half, so the substructure is there. But the two halves are disjoint index ranges — no subproblem is ever revisited, so there is no overlap. ⇒ This is Divide and Conquer, not DP. Caching would waste memory for zero speedup. (c) Max by scanning — neither (trivially). There is one linear pass; there is no recursion revisiting anything. Nothing to cache.
L1.2
A student memoizes longest simple path (no repeated vertices) between two nodes of a graph. Will the memo give correct answers?
Recall Solution
No. DP needs optimal substructure. For longest simple path it fails: the best path is not built from an independently-best plus best , because those pieces might reuse vertices, which "simple" forbids. Sub-answers are not composable, so caching them yields wrong results. (This is exactly the parent note's warning: prove the substructure before you memoize.)
Level 2 — Application
Goal: execute a given recurrence by hand and fill a table.
L2.1
Using , , , fill the tabulation array up to and give .
Recall Solution
Fill smallest → largest so each entry's dependencies already exist: for indices . So . Each cell used only — both already written. That's optimal substructure made concrete.
L2.2
0/1 Knapsack. Items (weight, value): , , , . Capacity . Fill the DP table and give the max value.
Recall the recurrence from the parent note:

Recall Solution
Rows = items considered , columns = capacity . Row is all zeros (no items). Reading the figure's grid:
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 0 (none) | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 (A 2,3) | 0 | 0 | 3 | 3 | 3 | 3 |
| 2 (B 3,4) | 0 | 0 | 3 | 4 | 4 | 7 |
| 3 (C 4,5) | 0 | 0 | 3 | 4 | 5 | 7 |
| 4 (D 5,6) | 0 | 0 | 3 | 4 | 5 | 7 |
Sample cell : skip → ; take → . Max . Answer: (take : weight , value ). never improve the last column here.
L2.3
Coin change (min coins). Coins , target . Fill min coins to make amount .
Recall Solution
. For , .
Answer: (coins ).
Level 3 — Analysis
Goal: reason about correctness and complexity.
L3.1
Why is naive Fibonacci but memoized Fibonacci ? Argue from the recursion tree.

Recall Solution
Naive: each node spawns two children, and no memory exists, so the tree roughly doubles per level → about nodes, each doing work ⇒ . The figure's left tree shows appearing twice, three times — pure repeated work. Memoized: the first time each is computed it fills the cache; every later request is an lookup (the greyed nodes in the right tree). There are only distinct values , each computed once ⇒ using the parent's formula
L3.2
The parent says knapsack DP is . If and , is that "polynomial and therefore fast"? Explain.
Recall Solution
is pseudo-polynomial, not truly polynomial. is a value, and its size in bits is , so the table has cells but the input is only bits. Here cells — infeasible, even though is tiny. Fast in , exponential in the number of bits of . (See Time Complexity Analysis.)
L3.3
In Bellman-Ford, why does relaxing all edges times correctly find shortest paths, and how is that a DP?
Recall Solution
Let shortest distance to using at most edges. Optimal substructure: a shortest -edge path to is a shortest -edge path to some neighbour , plus edge : A simple shortest path uses at most edges, so after full relaxation rounds every is final. Each round is one DP layer; overlap is huge because many edges feed the same vertex. That is exactly (#subproblems)(work) .
Level 4 — Synthesis
Goal: derive a recurrence yourself and justify both properties.
L4.1 — Longest Common Subsequence
For strings (length ) and (length ), derive length of the Longest Common Subsequence of the first chars of and first of . Then compute LCS of "AGCAT" and "GAC".
Recall Solution
Derivation. Look at the last characters .
- If : that char must extend an LCS of the shorter prefixes, so .
- If : at least one of them isn't in the common subsequence, so .
- Base: (empty prefix).
Optimal substructure: an optimal LCS of the prefixes is composed of an optimal LCS of shorter prefixes (swap-in argument). Overlap: and both recurse into — shared subproblems everywhere.
Compute AGCAT (rows), GAC (cols). Table (rows , cols ):
| ∅ | G | A | C | |
|---|---|---|---|---|
| ∅ | 0 | 0 | 0 | 0 |
| A | 0 | 0 | 1 | 1 |
| G | 0 | 1 | 1 | 1 |
| C | 0 | 1 | 1 | 2 |
| A | 0 | 1 | 2 | 2 |
| T | 0 | 1 | 2 | 2 |
LCS length (e.g. "AC" or "GA").
L4.2 — Climbing stairs (variant)
You can climb , , or steps at a time. How many distinct ways to reach step ? Give the recurrence and compute for .
Recall Solution
The last move landed from step , , or . Those are disjoint final moves, so we add the counts:
Answer: . Optimal substructure = "any full way ends with exactly one last hop"; overlap = each reused by three parents.
Level 5 — Mastery
Goal: combine properties, catch subtle failures, optimise.
L5.1 — Space optimisation
The knapsack table is . Show it can run in space, and state the one iteration rule that makes it correct.
Recall Solution
Each row reads only row . So keep a single 1-D array dp[0..W] and update it in place per item. But needs — an entry to its left and from the old row. If you iterate upward, you'd overwrite dp[c-w_i] to the new row before reading it (that would let one item be taken twice — the unbounded knapsack). To preserve the old left value for 0/1, iterate downward from to :
for i in range(n):
for c in range(W, w[i]-1, -1):
dp[c] = max(dp[c], v[i] + dp[c-w[i]])Rule: descend so each item is used at most once. Same time, now space.
L5.2 — Diagnose the failure
A student solves "longest increasing subsequence, but each element may be used twice" by memoizing on index only. It returns wrong answers on some inputs. Which property broke, and why?
Recall Solution
Optimal substructure broke relative to their state. Once "use each element ≤ twice" is added, the answer at index depends not just on but on how many times each value was already used — that extra history is part of the true subproblem. Memoizing on alone merges states that are actually different, so a cached value gets served in a context where it's invalid. Fix: enlarge the state to capture everything the future depends on (here, remaining usage budget), or prove the reduced state is sufficient. A correct memo key must make the subproblem self-contained.
L5.3 — Choose the paradigm
For fractional knapsack (, same items as L2.2, ), does DP beat greedy? Give the optimal value.
Recall Solution
For the fractional version, greedy is optimal and simpler than DP: sort by value/weight ratio and fill. Ratios: , , , . Take fully (uses 2, value 3, left 3), then fully (uses 3, value 4, left 0). Total weight , value . Here it ties the 0/1 answer, but in general fractional greedy 0/1 DP. Lesson: DP is not always the right hammer — when the greedy-choice property holds, greedy is both correct and cheaper.
Recall One-line self-test
Which property makes DP correct, and which makes it worth it? Correct ::: Optimal substructure (sub-answers compose into the true optimum). Worth it ::: Overlapping subproblems (the same sub-answer is reused, so caching saves work).
Connections
- Memoization vs Tabulation — the two implementation styles exercised above.
- Knapsack Problem · Longest Common Subsequence · Bellman-Ford — recurrences derived here.
- Time Complexity Analysis — pseudo-polynomial subtlety in L3.2.
- Recursion · Recursion Trees · Divide and Conquer · Greedy Algorithms — paradigm boundaries.