Repository Guide | English | 한국어
Last Updated: 2026-11-04
Introduction to Algorithms, Cormen, Leiserson, Rivest, and Stein, Ch 15, 25
Learning Objectives:
- Define dynamic programming and state its two required properties (optimal substructure, overlapping subproblems)
- Distinguish DP from divide-and-conquer and explain when each is appropriate
- Implement both top-down (memoization) and bottom-up (tabulation) DP strategies
- Solve the Fibonacci, matrix path, LCS, 0-1 knapsack, coin change, Floyd-Warshall, and edit distance problems using DP
- Formulate DP recurrences (define subproblems, identify base cases, and determine computation order) and analyze the time and space complexity of DP algorithms
- Compare DP with greedy algorithms and decide which approach to use for a given problem
- 1. Foundations of Dynamic Programming
- 2. Classic DP Problems
- 2.1 LCS: Problem Definition
- 2.2 LCS: Recurrence Relation
- 2.3 LCS: Pseudocode
- 2.4 LCS: Step-by-Step Example
- 2.5 0-1 Knapsack Problem
- 2.6 Knapsack: Recurrence
- 2.7 Knapsack: Worked Example
- 2.8 Coin Change Problem
- 2.9 Coin Change: Algorithm and Example
- 2.10 Floyd-Warshall: All-Pairs Shortest Paths
- 2.11 Floyd-Warshall: Algorithm
- 2.12 Floyd-Warshall: Example
- 2.13 Edit Distance (Levenshtein Distance)
- 2.14 Edit Distance: Example
- 3. DP Problem-Solving Strategy
- Summary
- Self-Check Questions
When recursion works well:
- Subproblem inputs sum to at most the original input size.
- Examples are merge sort, quick sort, factorial, and graph DFS.
When recursion is catastrophic:
- Subproblem inputs sum to more than the original input.
- Examples are Fibonacci numbers and matrix chain multiplication.
Key Point: Overlapping subproblems cause exponential redundant computation.
Dynamic Programming (DP) avoids redundant computation as follows.
- Solve small subproblems first.
- Store (memoize) their solutions.
- Use the stored solutions to solve larger subproblems.
- Build up to the original problem.
Two requirements for DP:
| Property | Description |
|---|---|
| Optimal Substructure | Optimal solution contains optimal solutions to subproblems |
| Overlapping Subproblems | Recursive solution re-solves the same subproblems repeatedly |
Table 1. Two requirements for DP
Key Point: DP solves each subproblem exactly once, so when the number of distinct subproblems is polynomial it turns an exponential-time recursive algorithm into a polynomial-time one. The "programming" in dynamic programming refers to a tabular method, not computer programming.
The longest simple path problem in an unweighted directed graph is an example without optimal substructure. In Figure 1, q → r → t is a longest simple path from q to t, but its subpath q → r is not a longest simple path from q to r (that path is q → s → t → r). Hence optimal solutions to subproblems cannot be joined into an optimal overall solution.
Figure 1. Directed graph showing that the longest simple path problem lacks optimal substructure
graph TD
subgraph dc["Divide & Conquer"]
a1["A"] --> b1["B"]
a1 --> c1["C"]
b1 --> d1["D"]
b1 --> e1["E"]
c1 --> f1["F"]
c1 --> g1["G"]
end
subgraph dp["DP"]
a2["A"] --> b2["B"]
a2 --> c2["C"]
b2 --> d2["D"]
b2 --> e2["E (shared)"]
b2 --> f2["F (shared)"]
c2 --> e2
c2 --> f2
c2 --> g2["G"]
end
Figure 2. Independent subproblems in divide-and-conquer and shared subproblems in DP
- In divide-and-conquer, subproblems are independent, so each is solved once.
- In DP, subproblems overlap, so each is solved once, stored, and reused.
- Divide-and-conquer splits a problem top-down, while DP usually computes bottom-up from small to large (it can also be implemented top-down, as with memoization in Section 1.4).
The recursive call tree in divide-and-conquer is a tree (no repeated nodes), while in DP the subproblem dependencies form a DAG (directed acyclic graph) with shared nodes. Figure 3 is the call tree obtained by directly running a recursion that splits a problem of size 4 into subproblems of sizes 3, 2, 1, and 0, and the same subproblems appear repeatedly (size 2 twice, size 1 four times, size 0 eight times). Figure 4 is the DAG that keeps each subproblem once and draws the dependencies as edges.
Figure 3. Call tree of the direct recursion for a problem of size 4
Figure 4. Subproblem DAG for a problem of size 4
Top-Down: Memoization
- It uses recursion but caches results in a table.
- It solves only the subproblems that are actually needed.
Bottom-Up: Tabulation
- It fills a table iteratively from the smallest subproblems.
- It is systematic and solves all subproblems in order.
Top-Down (Memoization) Bottom-Up (Tabulation)
┌──────────────────────┐ ┌──────────────────────┐
│ fib(n): │ │ f[1] = f[2] = 1 │
│ if memo[n] exists │ │ for i = 3 to n: │
│ return memo[n] │ │ f[i] = f[i-1] │
│ memo[n] = fib(n-1) │ │ + f[i-2] │
│ +fib(n-2) │ │ return f[n] │
│ return memo[n] │ │ │
└──────────────────────┘ └──────────────────────┘
Note: When to use which? Memoization is easier to implement (just add caching to recursion) and avoids solving unnecessary subproblems. Tabulation avoids recursion overhead and is typically faster in practice. Most DP problems can be solved either way.
Definition:
Naive recursion:
fib(n):
if n = 1 or n = 2: return 1
return fib(n-1) + fib(n-2)
Call tree for fib(7): the redundancy is enormous.
graph TD
f7["fib(7)"] --> f6["fib(6)"]
f7 --> f5a["fib(5)"]
f6 --> f5b["fib(5)"]
f6 --> f4a["fib(4)"]
f5a --> f4b["fib(4)"]
f5a --> f3["fib(3)"]
f5b --> e1["..."]
f4a --> e2["..."]
f4b --> e3["..."]
f3 --> e4["..."]
Figure 5. Call tree of naive recursion computing fib(7)
fib(5) is computed twice, fib(4) 3 times, and fib(3) 5 times.
Time complexity: exponential,
Memoization (Top-Down):
f[1] = f[2] = 1; all others = 0
fib(n):
if f[n] != 0: return f[n]
f[n] = fib(n-1) + fib(n-2)
return f[n]
Bottom-Up DP (Tabulation):
fibonacci(n):
f[1] = f[2] = 1
for i = 3 to n:
f[i] = f[i-1] + f[i-2]
return f[n]
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| f[i] | 1 | 1 | 2 | 3 | 5 | 8 | 13 |
Table 2. Fibonacci table f filled by bottom-up DP
Both achieve O(n) time, reducing exponential time to linear time.
Problem: Given an
Constraint: Only moves right or down are allowed (no left, up, or diagonal moves).
┌────┬────┬────┬────┐
│ 6 │ 7 │ 12 │ 5 │
├────┼────┼────┼────┤
│ 5 │ 3 │ 11 │ 18 │
├────┼────┼────┼────┤
│ 7 │ 17 │ 3 │ 3 │
├────┼────┼────┼────┤
│ 8 │ 10 │ 14 │ 9 │
└────┴────┴────┴────┘
Figure 6. Input matrix of the 4 × 4 matrix path problem
Recurrence:
Boundary:
Note: Optimal substructure. The best path to cell
$(i,j)$ must arrive from either the cell above$(i-1,j)$ or the cell to the left$(i,j-1)$ , whichever yields a higher cumulative sum.
matrixPath(n):
for i = 0 to n: c[i, 0] = 0
for j = 1 to n: c[0, j] = 0
for i = 1 to n:
for j = 1 to n:
c[i, j] = m[i,j] + max(c[i-1, j], c[i, j-1])
return c[n, n]
| c | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 6 | 13 | 25 | 30 |
| 2 | 0 | 11 | 16 | 36 | 54 |
| 3 | 0 | 18 | 35 | 39 | 57 |
| 4 | 0 | 26 | 45 | 59 | 68 |
Table 3. DP table c of the matrix path problem
Answer:
Longest Common Subsequence (LCS)
- A subsequence is obtained by deleting zero or more characters without changing the order of the remaining characters.
- A common subsequence of X and Y is a subsequence that appears in both strings.
- The LCS is the longest such common subsequence.
Example:
- X =
ABCBDAB, Y =BDCABA - Common subsequence:
BCA(length 3) - LCS:
BCBA(length 4)
Goal: Find the length of the LCS (and optionally the sequence itself).
Let
Define
Intuition:
- If the last characters match, extend the LCS of the shorter prefixes by 1.
- If they do not match, take the better result of dropping either character.
LCS(m, n):
for i = 0 to m: C[i, 0] = 0 // empty Y
for j = 0 to n: C[0, j] = 0 // empty X
for i = 1 to m:
for j = 1 to n:
if x[i] = y[j]:
C[i, j] = C[i-1, j-1] + 1
else:
C[i, j] = max(C[i-1, j], C[i, j-1])
return C[m, n]
Time:
Space:
X = ABCBDAB, Y = BDCABA
| c | B | D | C | A | B | A | |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | |
| A | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
| B | 0 | 1 | 1 | 1 | 1 | 2 | 2 |
| C | 0 | 1 | 1 | 2 | 2 | 2 | 2 |
| B | 0 | 1 | 1 | 2 | 2 | 3 | 3 |
| D | 0 | 1 | 2 | 2 | 2 | 3 | 3 |
| A | 0 | 1 | 2 | 2 | 3 | 3 | 4 |
| B | 0 | 1 | 2 | 2 | 3 | 4 | 4 |
Table 4. LCS table c for X = ABCBDAB and Y = BDCABA
LCS length = 4 (e.g., BCBA). Bold cells show where
Figure 7. LCS table with traceback arrows
Note: Traceback. To recover the actual LCS (not just its length), trace back from
$c[m,n]$ : if$x_i = y_j$ , include that character and move diagonally; otherwise, move in the direction of the larger value. The shaded cells in Figure 7 are the path followed by this rule.
Problem: Given
Example: Capacity
| Item | Weight | Value |
|---|---|---|
| 1 | 5 kg | 10 |
| 2 | 4 kg | 40 |
| 3 | 6 kg | 30 |
| 4 | 3 kg | 50 |
Table 5. Items of the 0-1 knapsack example
Subproblem:
Answer:
Note: Why greedy fails for 0-1 knapsack. The greedy approach (sort by value/weight ratio) does not work because items cannot be divided. Taking a high-ratio item might prevent taking a combination of items with higher total value.
For each item
Two choices for item
-
If it is not taken, the value is
$K[i-1, w]$ . -
If it is taken, the value is
$K[i-1, w - w_i] + v_i$ (make room and add its value).
Knapsack(n, C):
for i = 0 to n: K[i, 0] = 0
for w = 0 to C: K[0, w] = 0
for i = 1 to n:
for w = 1 to C:
if w[i] > w: K[i,w] = K[i-1, w]
else: K[i,w] = max(K[i-1,w], K[i-1,w-w[i]] + v[i])
return K[n, C]
Items (weight, value): (5kg, 10), (4kg, 40), (6kg, 30), (3kg, 50). Capacity = 10.
| K[i,w] | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 (5,10) | 0 | 0 | 0 | 0 | 0 | 10 | 10 | 10 | 10 | 10 | 10 |
| 2 (4,40) | 0 | 0 | 0 | 0 | 40 | 40 | 40 | 40 | 40 | 50 | 50 |
| 3 (6,30) | 0 | 0 | 0 | 0 | 40 | 40 | 40 | 40 | 40 | 50 | 70 |
| 4 (3,50) | 0 | 0 | 0 | 50 | 50 | 50 | 50 | 90 | 90 | 90 | 90 |
Table 6. DP table K of the 0-1 knapsack example
Optimal value:
Time:
Problem: Given coin denominations
Greedy can fail. Example: coins = {16, 10, 5, 1}, amount = 20.
- Greedy: 16 + 1 + 1 + 1 + 1 = 5 coins
- Optimal: 10 + 10 = 2 coins
DP Recurrence:
Base case:
DPCoinChange(n, d[1..k]):
C[0] = 0
for j = 1 to n: C[j] = infinity
for j = 1 to n:
for i = 1 to k:
if d[i] <= j and C[j - d[i]] + 1 < C[j]:
C[j] = C[j - d[i]] + 1
return C[n]
Example: coins = {16, 10, 5, 1}, amount = 20
| j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ... | 16 | ... | 20 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| C[j] | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 1 | ... | 1 | ... | 2 |
Table 7. DP table C for making amount 20 with coins {16, 10, 5, 1}
Answer:
Problem: Find shortest paths between all pairs of vertices in a weighted directed graph (assuming no negative-weight cycles).
Key idea: Incrementally allow intermediate vertices
Recurrence:
-
$d_{ij}^{(0)} = w_{ij}$ (direct edge weight, or$\infty$ if no edge) $d_{ii}^{(0)} = 0$
Interpretation: Is it shorter to go from
FloydWarshall(W, n):
// W[i,j] = edge weight (infinity if no edge)
D = W // initialize D^(0)
for k = 1 to n: // add vertex k as intermediate
for i = 1 to n:
for j = 1 to n:
D[i,j] = min(D[i,j], D[i,k] + D[k,j])
return D
Time:
Space:
[Data Structures] The input
$W$ is the adjacency matrix representation of the graph, which stores a graph with$n$ vertices in an$n \times n$ array so that the weight of edge$(i, j)$ is read from$W[i,j]$ in$O(1)$ time. Floyd-Warshall copies this matrix into the distance matrix$D$ and looks up$D[i,k]$ and$D[k,j]$ in constant time at every update, so each step of the three nested loops takes$O(1)$ and the total time is$O(n^3)$ .
Note: Each iteration
$k$ considers all paths that may pass through vertices${1, 2, \ldots, k}$ as intermediates.
Initial D^(0): After k=1,2,3,4: D^(4):
| 1 2 3 4 | 1 2 3 4
|------------------- |-------------------
1| 0 3 inf 7 1| 0 3 5 6
2| inf 0 2 inf 2| 5 0 2 3
3| 5 inf 0 1 3| 3 6 0 1
4| 2 inf inf 0 4| 2 5 7 0
Figure 8. Initial distance matrix D(0) and final distance matrix D(4) of the Floyd-Warshall example
Each cell
Problem: Given two strings
Operations (each costs 1):
- Insert a character.
- Delete a character.
- Replace a character.
DP Recurrence:
-
$E[i-1,j] + 1$ corresponds to deleting$X[i]$ . -
$E[i,j-1] + 1$ corresponds to inserting$Y[j]$ . -
$E[i-1,j-1] + 1$ corresponds to replacing$X[i]$ with$Y[j]$ .
Note: Applications. Edit distance is widely used in spell checking, DNA sequence alignment, diff tools, and natural language processing.
Transform kitten into sitting (m=6, n=7).
| E | s | i | t | t | i | n | g | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
| k | 1 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| i | 2 | 2 | 1 | 2 | 3 | 4 | 5 | 6 |
| t | 3 | 3 | 2 | 1 | 2 | 3 | 4 | 5 |
| t | 4 | 4 | 3 | 2 | 1 | 2 | 3 | 4 |
| e | 5 | 5 | 4 | 3 | 2 | 2 | 3 | 4 |
| n | 6 | 6 | 5 | 4 | 3 | 3 | 2 | 3 |
Table 8. Edit distance table E for transforming kitten into sitting
Answer:
- Replace k with s.
- Replace e with i.
- Insert g at the end.
Time:
Step-by-step approach to any DP problem:
-
Define the subproblem: What does
$OPT[i, j, \ldots]$ represent? -
Find the recurrence: How does
$OPT[\cdot]$ relate to smaller subproblems? - Identify base cases: What are the trivial or boundary values?
- Determine computation order: Fill the table bottom-up (small to large).
- Extract the answer: Where in the table is the final solution?
- (Optional) Trace back: Reconstruct the actual solution, not just its value.
Table 9 summarizes the subproblem, recurrence, and running time of the problems covered above.
| Problem | Subproblem | Recurrence | Time |
|---|---|---|---|
| Fibonacci | |||
| Matrix Path | |||
| LCS | match: |
||
| 0-1 Knapsack | |||
| Coin Change | |||
| Floyd-Warshall | |||
| Edit Distance | match: |
Table 9. Subproblems, recurrences, and running times of representative DP problems
Note: An algorithm that remembers the past. George Santayana wrote, "Those who cannot remember the past are condemned to repeat it." Dynamic programming follows the same idea: it remembers the solutions of subproblems already solved so that it never repeats the same computation.
| Concept | Key Summary |
|---|---|
| Dynamic programming | It solves optimization problems by storing and combining the solutions of overlapping subproblems. |
| Requirements | It requires both optimal substructure and overlapping subproblems. |
| Computation order | Subproblems have an implicit order, so smaller ones must be solved first. |
| Implementation strategies | There are two strategies, memoization (top-down) and tabulation (bottom-up). |
| DP vs greedy | DP considers all subproblem combinations, while greedy makes one local choice. |
| DP vs divide-and-conquer | DP reuses subproblem solutions, while divide-and-conquer subproblems are independent. |
| Representative problems | Fibonacci runs in O(n), matrix path in |
-
Optimal Substructure: In your own words, explain what "optimal substructure" means. Give an example of a problem that has it and one that does not.
Answer
A problem has optimal substructure when its optimal solution can be assembled from optimal solutions to its subproblems. Shortest path has it: any subpath of a shortest
$s \to t$ path is itself a shortest path between its endpoints, which is why both Floyd-Warshall and Dijkstra work. Longest simple path does not: joining a longest simple$s \to u$ path with a longest simple$u \to t$ path may revisit vertices, so combining locally optimal subpaths does not yield a globally optimal simple path (Figure 1 in Section 1.2). -
Memoization vs Tabulation: What are the trade-offs between top-down (memoization) and bottom-up (tabulation) DP? When might you prefer one over the other?
Answer
Memoization keeps the natural recursive form and only solves subproblems that are actually reached, which is useful when the reachable subset is sparse (e.g., many states unreachable from the root call). Its downsides are recursion overhead and stack depth limits, which can blow up for deep recurrences. Tabulation iterates over all states in dependency order, avoiding recursion and giving better cache locality and constant factors, but may waste work on states the answer never needs. Prefer memoization when the state space is large but sparsely visited; prefer tabulation when most states are needed or when stack depth is a concern.
-
Fibonacci: Draw the call tree for
fib(6)using naive recursion. How many times isfib(3)computed? Then show how memoization eliminates the redundancy.Answer
The tree for
fib(6)expands asfib(6) -> fib(5) + fib(4), thenfib(5) -> fib(4) + fib(3), and so on down to the base cases.fib(3)is computed 3 times: once under eachfib(4)(there are two) and once directly underfib(5). Memoization storesf[n]the first time it is computed, so every subsequent call returns in$O(1)$ . The number of distinct subproblems is just$n$ , collapsing the call tree from$O(2^n)$ exponential work to$O(n)$ linear work. -
LCS: Compute the LCS table for X =
ABCBand Y =BDCB. What is the LCS length? Trace back to find the actual LCS.Answer
Filling
$c[i,j]$ using$c[i-1,j-1]+1$ on match and$\max(c[i-1,j],c[i,j-1])$ otherwise yields the final row/column with$c[4,4] = 3$ . LCS length = 3. Traceback from$c[4,4]$ :$X_4 = B = Y_4$ → takeB, move diagonal;$X_3 = C = Y_3$ → takeC, move diagonal;$X_2 = B = Y_1$ after a horizontal step → takeB. Reversing gives LCS =BCB. -
0-1 Knapsack: Given items (weight, value): (2, 12), (1, 10), (3, 20), (2, 15) and capacity 5, fill the DP table. What is the optimal value and which items are selected?
Answer
Applying
$K[i,w] = \max(K[i-1,w],\ K[i-1, w-w_i]+v_i)$ row by row gives$K[1,5]=12$ ,$K[2,5]=22$ ,$K[3,5]=32$ , and$K[4,5] = 37$ . Traceback:$K[4,5] \neq K[3,5]$ → take item 4 (w=2, v=15), capacity drops to 3;$K[3,3] = K[2,3] = 22$ → skip item 3;$K[2,3] \neq K[1,3]$ → take item 2 (w=1, v=10), capacity 2;$K[1,2] \neq K[0,2]$ → take item 1 (w=2, v=12). Selected items: {1, 2, 4} with total weight$2+1+2 = 5$ and total value $12+10+15 = $ 37. -
Coin Change: With denominations {25, 10, 6, 1}, find the minimum coins for amount 30 using DP. Show why the greedy approach (largest coin first) gives a suboptimal answer.
Answer
Greedy picks 25 first, leaving 5 → then 1+1+1+1+1, totaling 6 coins. DP computes
$C[j] = \min_{d_i \leq j}(C[j-d_i] + 1)$ and at$j=30$ tries each denomination:$C[30-10]+1 = C[20]+1 = 3$ , since$C[20] = 2$ via two 10-coins. So $C[30] = $ 3 coins (10+10+10). Greedy fails because committing to 25 strands the remainder on a denomination set with no efficient way to make 5; DP considers all coin choices at each amount, so it finds the better global combination that avoids 25 entirely. -
Floyd-Warshall: Using the 4-vertex graph of Section 2.12, trace through the algorithm for one iteration (k=2). Which cells change and why?
Answer
Iteration
$k=2$ updates$D[i,j] \gets \min(D[i,j],\ D[i,2] + D[2,j])$ for every pair$(i,j)$ . In the example of Section 2.12,$D^{(1)}$ after iteration$k=1$ differs from$D^{(0)}$ only in$D[3,2] = 8$ and$D[4,2] = 5$ . At$k=2$ ,$D^{(1)}[3,4] = 1$ stays,$D^{(1)}[1,3] = \infty$ becomes$\min(\infty, D[1,2]+D[2,3]) = \min(\infty, 3+2) = 5$ , and$D^{(1)}[4,3] = \infty$ becomes$\min(\infty, D[4,2]+D[2,3]) = \min(\infty, 5+2) = 7$ .$D[1,4] = 7$ is unchanged since$D[2,4] = \infty$ at this step. Cells that change are exactly those whose shortest path can now route through vertex 2; cells whose current best already bypasses or beats that route stay the same. -
Edit Distance: Compute the edit distance between
sundayandsaturday. Show the DP table and identify the three operations needed.Answer
With
$X$ =sunday(m=6),$Y$ =saturday(n=8), the recurrence gives$E[6,8] = 3$ . Aligning the strings:smatches, thenaandtmust be inserted beforeu(2 inserts), thenu-n-d-a-yaligns withu-r-d-a-yrequiring one replace ofn → r. So the three operations are inserta, insertt, and replacenwithr, which is exactly the minimum of 3 single-character edits.



