13. Dynamic programming
Dynamic programming (DP) is a way of solving a problem by breaking it into smaller subproblems, computing each subproblem exactly once, and storing its answer to be reused.
Two conditions a problem must meet to be solvable with DP:
- Overlapping subproblems. The same subproblem
appears many times — like
fib(43), which was computed hundreds of millions of times in Chapter 06. - Optimal substructure. The best answer to the large problem is built from the best answers to its subproblems.
The shortest way to remember what DP is:
DP = brute force − repeated work.
Chapter 06 already showed the difference in numbers.
fib(45) written as plain recursion needs 3,672,623,805
calls, even though there are only 46 truly distinct answers:
fib(0) through fib(45). The rest is
recomputation. Once every answer is stored, the work drops from billions
to dozens of steps — and that is the whole idea of DP.
Two writing styles
Top-down (memoization). Write ordinary recursion, then add memory. You already did this in the Chapter 06 challenge.
long long ingat[100];
long long fib(int n) {
if (n <= 1) return n;
if (ingat[n] != -1) return ingat[n];
return ingat[n] = fib(n - 1) + fib(n - 2);
}Bottom-up (tabulation). Fill a table from the smallest subproblem to the largest, without recursion.
long long dp[100];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];Both compute the same thing:
| Top-down (memoization) | Bottom-up (tabulation) | |
|---|---|---|
| form | recursion + memory array | loop filling a table |
| fill order | handled by the recursion itself | you have to work it out |
| unused states | never computed | computed anyway |
| risk | call stack runs out | none |
| speed | slightly slower | slightly faster |
| most convenient when | the recursion is already written | the state is simple and the order is clear |
Top-down is easier to write from existing recursion; bottom-up is faster and has no risk of running out of stack memory. This book uses bottom-up for the following examples.
The memory array in top-down must be filled with a "not computed yet"
marker value that can never be a real answer. Using
0 as the marker in a problem whose answer can be
0 makes that state get recomputed over and over — and the
speed advantage disappears without a single wrong answer, so the mistake
only shows up as Time Limit Exceeded.
Five steps to build a DP
Every time you face a DP problem, answer these five questions on paper before writing code:
- What is the state? What does
dp[i]mean? Write it as a complete sentence. - What is the base case? The smallest state whose answer is already known.
- What is the transition? How
dp[i]is computed from smaller states. - What is the fill order? Every state that is used must already be filled.
- Which state holds the answer?
Step 1 is the hardest, and the rest usually follows.
Example 1: climbing stairs
There is a staircase with
nsteps. Each move goes up 1 or 2 steps. How many different ways are there to reach stepn?
- State.
dp[i]= the number of ways to reach stepi. - Base case.
dp[0] = 1(one way: stay at the bottom),dp[1] = 1. - Transition. To arrive at
i, the last move comes fromi − 1or fromi − 2. Sodp[i] = dp[i-1] + dp[i-2]. - Order. From small to large.
- Answer.
dp[n].
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
cout << dp[n] << "\n";Filling the table for n = 6:
i |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
dp[i] |
1 | 1 | 2 | 3 | 5 | 8 | 13 |
Check by hand for i = 3: there are three ways to reach
step 3, namely 1+1+1, 1+2, and 2+1. This matches
dp[3] = 3.
The result is the Fibonacci sequence again — a sign that many problems that look different actually have the same state.
Open
exercises/13/01-tangga.cpp
and change it so a move may be 1, 2, or 3 steps. Only the transition
line changes.
Example 2: fewest coins
This is the problem that made greedy fail in Chapter 12.
Given coin denominations
c[1..m]and a valuek. What is the fewest number of coins that make exactlyk? Print-1if it is impossible.
- State.
dp[v]= the fewest coins to make valuev. - Base case.
dp[0] = 0. - Transition.
dp[v] = 1 + min(dp[v - c[i]])over every coinc[i]that does not exceedv. - Order.
vfrom 1 tok. - Answer.
dp[k], or-1if its value is still infinite.
const int BESAR = 1000000000;
dp[0] = 0;
for (int v = 1; v <= k; v++) {
dp[v] = BESAR;
for (int i = 0; i < m; i++) {
if (v >= c[i] && dp[v - c[i]] + 1 < dp[v])
dp[v] = dp[v - c[i]] + 1;
}
}
cout << (dp[k] >= BESAR ? -1 : dp[k]) << "\n";Filling the table for coins {1, 3, 4}:
v |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
dp[v] |
0 | 1 | 2 | 1 | 1 | 2 | 2 |
How to read one cell: dp[6] checks three possibilities
for the last coin — coin 1 gives dp[5] + 1 = 3, coin 3
gives dp[3] + 1 = 2, coin 4 gives
dp[2] + 1 = 3. The smallest is 2, so
dp[6] = 2.
Notice that DP does not need to know which coin "should" be taken first. It tries all of them as the last coin, then uses the subproblem answers that are already proven best. That is where it differs from greedy, which only tries one choice and never goes back.
The number of steps is k × m: one state per value, and
each state checks m coins. For k = 10⁶ and
m = 10 that is 10 million — fast.
Open
exercises/13/02-koin.cpp.
Add a loop that prints all of dp[0..k], run it for coins
1 3 4 and k = 10, then compare against the
first row of the table above. Note that dp[10] = 3, whereas
greedy answers 4.
Compare with greedy for {1, 3, 4} and
k = 6: DP answers 2, greedy answers 3. DP checks every
possibility without repeating work.
Example 3: 0/1 knapsack
There are
nitems; itemihas weightw[i]and valuev[i]. The bag can only hold weightW. Each item may be taken at most once. What is the largest value that can be carried?
State.
dp[i][b]= the largest value using the firstiitems with weight limitb.Base case.
dp[0][b] = 0for allb.Transition. Two choices for item
i:- not taken:
dp[i-1][b] - taken (if
b >= w[i]):dp[i-1][b - w[i]] + v[i]
Take the larger one.
- not taken:
Order.
ifrom 1 ton,bfrom 0 toW.Answer.
dp[n][W].
for (int i = 1; i <= n; i++) {
for (int b = 0; b <= W; b++) {
dp[i][b] = dp[i - 1][b]; // not taken
if (b >= w[i] && dp[i - 1][b - w[i]] + v[i] > dp[i][b])
dp[i][b] = dp[i - 1][b - w[i]] + v[i]; // taken
}
}The number of steps is n × W. For n = 100
and W = 10,000 that is a million.
Tracing for W = 5 and three items — (weight 2, value 3),
(weight 3, value 4), (weight 4, value 5). Each row is the state after
item i has been considered:
dp[i][b] |
b=0 | b=1 | b=2 | b=3 | b=4 | b=5 |
|---|---|---|---|---|---|---|
i=0 (no items) |
0 | 0 | 0 | 0 | 0 | 0 |
i=1 (weight 2) |
0 | 0 | 3 | 3 | 3 | 3 |
i=2 (+ weight 3) |
0 | 0 | 3 | 4 | 4 | 7 |
i=3 (+ weight 4) |
0 | 0 | 3 | 4 | 5 | 7 |
The answer is dp[3][5] = 7, namely the first and second
items (weight 2 + 3 = 5, value 3 + 4 = 7). Trace one cell to make sure
you understand: dp[2][5] chooses between not taking the
second item (dp[1][5] = 3) and taking it
(dp[1][5-3] + 4 = dp[1][2] + 4 = 7). The larger one is
7.
Notice also that each row only reads the row directly above
it. That property is what lets the knapsack table be compressed
into a single row to save memory — a technique that is useful when
n is large, but should not be used before the
two-dimensional version is fully mastered.
Knapsack is the best example of why greedy is not enough. Taking the item with the highest value per weight sounds reasonable, but it can be wrong: two "less efficient" items often fill the bag better than one large "efficient" item. DP tries both possibilities for every item.
Open
exercises/13/03-knapsack.cpp.
It contains a greedy version (value per weight) and a DP version. Run
the sample data and see that greedy gives a smaller answer.
Counting DP steps
The formula is always:
number of steps = number of states × cost of one transition
| Problem | States | Transition | Total |
|---|---|---|---|
| climbing stairs | n | O(1) | O(n) |
| fewest coins | k | O(m) | O(k · m) |
| 0/1 knapsack | n × W | O(1) | O(n · W) |
Use that formula together with the constraints table from Chapter 10: if the number of states alone is already 10¹⁰, the state needs to be redesigned.
Recognizing DP problems
Four signs that a problem is most likely DP:
- The problem asks for the most, the fewest, or how many ways — not "print one example".
- Each step is a choice (take or not, move 1 or 2, cut here or not).
- A brute force that tries all choices is clearly correct, but the number of possibilities is 2ⁿ or n!.
- The choices already made affect the rest only through a small amount of information — for example "remaining bag capacity" or "which step we have reached", not the entire history of choices.
The fourth sign is the most decisive, because that small amount of
information becomes the dp state. If the rest of the
problem turns out to depend on the entire history of choices, the state
will explode and the problem is not a simple DP.
Comparison with the two previous approaches:
| Approach | Tries | Cost |
|---|---|---|
| brute force | all possibilities, repeatedly | 2ⁿ or n! |
| greedy | only one possibility | n log n, often wrong |
| DP | all possibilities, each once | states × transition |
Most common mistakes
- The state is not defined as a sentence. If
dp[i]cannot be explained in one sentence, the transition will certainly be a mess. - Wrong base case.
dp[0] = 0anddp[0] = 1are two different problems: one computes a sum, the other counts ways. - Wrong fill order, so a state that is used has not been filled yet.
- Array too small, so the answer for the last
kgets overwritten.
Homework
Problem 1 — Climbing stairs by threes
Open
exercises/13/homework/01-tangga.cpp.
Read n (≤ 1,000,000). Print the number of ways to reach
step n when each move may go up 1, 2, or 3 steps. The
answer is large, so print its remainder modulo 1,000,000,007 (how this
works is covered in Chapter 14; for now it is enough to take
% 1000000007 every time you add).
Problem 2 — Fewest coins
Open
exercises/13/homework/02-koin.cpp.
Read m and k, then m coin
denominations. Print the fewest number of coins to make exactly
k, or -1 if it is impossible. Test with coins
1 3 4 and k = 6; the answer must be 2.
Problem 3 — Number of ways
Open
exercises/13/homework/03-cara-koin.cpp.
With the same input as problem 2, print the number of
ways to make value k, where the order of picking
does not count as different (3 + 1 is the same as
1 + 3). Hint: the outer loop goes over the coins, the inner
loop goes over the values. If the order is reversed, what gets counted
is distinct orderings.
Problem 4 — 0/1 knapsack
Open
exercises/13/homework/04-knapsack.cpp.
Read n and W, then n pairs of
weight and value. Print the largest value that can be carried.
Challenge — Longest increasing subsequence
Open
exercises/13/homework/05-lis.cpp.
Read n (≤ 5,000) then n numbers. Print the
length of the longest increasing subsequence — that is, the largest
number of elements that can be chosen while keeping their original order
and with values always increasing. Example: 3 1 4 1 5 9 2 6
gives 4 (for example 1 4 5 9). The state:
dp[i] = the length of the longest increasing subsequence
that ends at element i.
OSN tips
The first sign of a DP problem: "how many ways", "fewest", or "largest", with stacked choices. If every step presents several choices and the current choice affects the next ones, greedy usually fails and DP usually works.
State the state in one sentence before writing code.
That sentence must have the form "dp[...] is the ... of the
first ...". If the sentence cannot be written yet, the code must not be
written yet. Almost all DP mistakes come from an unclear state, not from
typos.
Compute the table size before creating it. The number of states multiplied by the cost per state is the number of steps, and the number of states multiplied by the type size is the memory:
| Table | States | Memory (long long) |
|---|---|---|
dp[200000] |
2 × 10⁵ | 1.6 MB |
dp[1000][1000] |
10⁶ | 8 MB |
dp[100][10000] |
10⁶ | 8 MB |
dp[5000][5000] |
2.5 × 10⁷ | 200 MB — almost always over the limit |
If the table is too large, often only the last one or two rows are actually needed. Problem 2 below is an example.
Check the initial state separately. The most common
DP mistake is not in the recurrence formula but in the initial values:
dp[0] should be 1 or 0, an upper bound should be
"impossible" rather than zero, and the first cell of a grid should be
checked for being blocked.
The loop direction determines the meaning. In a DP that uses one array to save memory, a backward loop means "each item is used at most once", while a forward loop means "items may be used multiple times". Two different problems, two different directions, nearly identical code.
Test the DP against brute force. For N
≤ 15, the brute force answer is certainly correct. Running both on
hundreds of small random inputs catches initial-state mistakes and
boundary mistakes that do not show up in the sample tests.
If your DP answer is slightly off, check these three places first
before changing the formula: the very first dp value, the
loop bounds (whether they include the last element), and whether the
answer is taken from the correct state — often dp[n-1] is
printed when it should be dp[n], or the other way
around.
Summary
- DP is brute force minus repeated work: every subproblem is computed exactly once and its answer is stored.
- The conditions are overlapping subproblems and optimal substructure.
- Two styles: top-down (recursion + memory) and bottom-up (filling a table). Both compute the same thing.
- Five questions answered on paper: state, base case, transition, fill order, where the answer is. The state must be writable as one complete sentence.
- Number of steps = number of states × cost of one transition; compare with the constraints table from Chapter 10 before writing code.
- Signs of a DP problem: it asks for the most/fewest/how many ways, each step is a choice, and the rest of the problem depends only on a small amount of information.
OSN-style exercises
The following three problems represent the three most common forms of DP: counting the number of paths on a grid, choosing with a restriction, and checking whether a sum can be formed.
Skeletons for all three are in
exercises/13/osn/.
Problem 1 — Grid Paths with Obstacles
A map is laid out as a grid of R rows and C
columns. Some cells can be passed through, some are blocked. An ant is
at the top-left cell and wants to reach the bottom-right cell. From a
cell, it may only move right or down, and it may not step on a blocked
cell.
Count how many different paths it can take.
Input format. The first line contains two integers
R and C. Each of the next R lines
contains C characters: . for a passable cell
and # for a blocked cell.
Output format. One line containing the number of
paths. If there is no path at all, print 0.
Constraints. 1 ≤ R, C ≤
17.
Sample input
3 3
...
.#.
...
Sample output
2
Explanation. Without obstacles there are six paths. The blocked center cell cuts off four of them, leaving two: along the top edge then the right edge, or the left edge then the bottom edge.
Skeleton:
exercises/13/osn/01-jalur-grid.cpp
Problem 2 — No Neighbors
Along one side of a street stand N stalls in a row, and
stall i yields a profit of Aᵢ if rented. The
owner forbids two adjacent stalls from being rented by the same
person.
Determine the largest total profit that can be obtained by renting a set of stalls in which no two are adjacent. Renting nothing at all is also allowed.
Input format. The first line contains an integer
N. The second line contains N integers
A₁ A₂ … A_N.
Output format. One line containing the largest total profit.
Constraints.
- 1 ≤
N≤ 200,000 - 0 ≤
Aᵢ≤ 10⁹
Sample input
6
3 2 7 10 12 5
Sample output
22
Explanation. The best choice is stalls 1, 3, and 5: 3 + 7 + 12 = 22. The choice 3 + 10 + 5 only yields 18, and 10 + 12 is not allowed because the two stalls are adjacent.
Skeleton:
exercises/13/osn/02-tanpa-bertetangga.cpp
Problem 3 — Split into Two Equal Weights
The organizers have N sacks of known weight, and two
identical trucks. All sacks must be loaded, and the organizers want both
trucks to carry exactly the same weight. Sacks may not
be split.
Determine whether such a split is possible.
Input format. The first line contains an integer
N. The second line contains N integers, the
weight of each sack.
Output format. One line containing YA
("yes") or TIDAK ("no").
Constraints.
- 1 ≤
N≤ 100 - 1 ≤ weight ≤ 100
Sample input
4
1 5 11 5
Sample output
YA
Explanation. The first truck carries sacks 1, 2, and
4 with a total of 1 + 5 + 5 = 11; the second truck carries sack 3, which
weighs 11. For input 1 2 5, the answer is
TIDAK because the total is 8 but no subset sums to 4.
Skeleton:
exercises/13/osn/03-bagi-dua.cpp
Stuck or already done? Open the homework solutions page.