Topics › Dynamic Programming › Introduction to DP
Introduction to DP
Java source ↗ Edit this article ↗
Jump to code:
What dynamic programming is
Dynamic programming (DP) is recursion that never solves the same subproblem twice. You describe the answer to a problem in terms of answers to smaller versions of the same problem (a recurrence), and you store each smaller answer the first time you compute it. That is all it is. The hard part is never the caching; it is finding the right recurrence.
A problem is a good fit for DP when two things are true:
- Overlapping subproblems. A plain recursion reaches the same state (the same arguments) many times. Caching only pays off if there is something to reuse.
- Optimal substructure. The answer for a state can be built from the answers of smaller states, and those smaller answers do not depend on how you got to them.
Merge sort is a useful contrast. It also splits a problem into smaller ones, but the two halves never overlap, so a cache would never get a hit. That is divide and conquer, not DP.
In practice, DP problems ask you to count the ways to do something, find the minimum or maximum cost of doing it, or decide whether it can be done at all, where the input forces a sequence of choices.
This article uses the Fibonacci numbers to show the four forms every DP solution in this sheet goes through: plain recursion, memoization, tabulation and space optimization. Every method lives in the Java file and is checked against known values.
The running example: Fibonacci
F(0) = 0, F(1) = 1, and F(n) = F(n - 1) + F(n - 2) for n ≥ 2. The sequence starts 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55.
n = 5 -> 5
n = 10 -> 55
Step 0: Plain recursion
Translate the definition directly. The two base cases stop the recursion; every other call makes two recursive calls. The static counter calls is only there so the test can measure how much work this does.
static long plainRecursion(int n) {
calls++;
if (n <= 1) return n; // base cases: F(0) = 0, F(1) = 1
return plainRecursion(n - 1) + plainRecursion(n - 2);
}
Here is the call tree for n = 4:
f(4)
/ \
f(3) f(2) <- f(2) computed here...
/ \ / \
f(2) f(1) f(1) f(0)
/ \
f(1) f(0) <- ...and again here
Nine calls to produce the number 3. f(2) is solved twice and f(1) three times. The waste compounds: f(n - 2) is solved once directly and again inside f(n - 1), so the number of calls obeys the same recurrence as Fibonacci itself. Exactly 2F(n + 1) - 1 calls are made, which grows like 1.618^n:
| n | calls |
|---|---|
| 10 | 177 |
| 20 | 21,891 |
| 30 | 2,692,537 |
| 40 | 331,160,281 |
| 50 | about 4 × 10^10 |
Yet there are only n + 1 distinct subproblems, f(0) through f(n). That gap between “calls made” and “distinct states” is the signal that DP applies.
Time O(2^n) (more precisely O(1.618^n)), stack space O(n) because the deepest chain is f(n), f(n-1), ..., f(1).
Technique 1: Memoization (top-down)
Keep the recursion exactly as it is and add a table. Three edits turn any recursion into memoized recursion:
- Create a table
dpwith one cell per possible state, filled with a value that cannot be a real answer (here-1). - At the start of the call, if
dp[n]is filled, return it. - Before returning a freshly computed answer, store it in
dp[n].
static long memoization(int n) {
long[] dp = new long[n + 1];
Arrays.fill(dp, -1); // -1 means "not computed yet"
return memo(n, dp);
}
static long memo(int n, long[] dp) {
memoCalls++;
if (n <= 1) return n;
if (dp[n] != -1) return dp[n]; // solved before: answer in O(1)
return dp[n] = memo(n - 1, dp) + memo(n - 2, dp); // solve once, store, return
}
Why does this make only 2n - 1 calls? memo(n) first calls memo(n - 1), which recursively fills every cell from 2 up to n - 1. When control comes back and memo(n) asks for memo(n - 2), the answer is already in the table, so that call returns at once. The tree collapses into a chain with one short leaf hanging off each level. For n = 20 the test asserts 39 calls instead of 21,891.
Time O(n): each of the n + 1 states is computed once, in O(1) work besides its recursive calls. Space O(n) for the table plus O(n) for the recursion stack.
The sentinel must be impossible as a real answer. Fibonacci numbers are never negative, so -1 is safe. When answers can be negative (minimum path sums, for example) use something like Integer.MAX_VALUE or a Boolean/boxed table instead.
Technique 2: Tabulation (bottom-up)
Memoization works backwards from n and lets recursion discover the order. Tabulation flips the direction: start at the base cases and fill the table forward, in an order where every cell’s dependencies are already filled when you reach it. For Fibonacci, dp[i] needs dp[i - 1] and dp[i - 2], so a plain left-to-right loop works.
The recipe is mechanical:
- Declare the same table the memoized version used.
- Write the base cases into it.
- Loop over the states in dependency order.
- Copy the recurrence, replacing each recursive call with a table read.
static long tabulation(int n) {
if (n <= 1) return n;
long[] dp = new long[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
Dry run for n = 6:
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 1 | 2 | 3 | 5 | 8 |
Time O(n), space O(n) for the table, and no recursion at all. That last point matters in Java: a memoized recursion 100,000 levels deep will usually throw StackOverflowError with the default thread stack, while the loop does not care how large n is.
Technique 3: Space optimization
Look at what the loop reads: only the previous two cells. Everything older is dead weight. Keep two variables instead of the array:
static long spaceOptimized(int n) {
if (n <= 1) return n;
long prev2 = 0, prev = 1; // F(i - 2) and F(i - 1), starting at i = 2
for (int i = 2; i <= n; i++) {
long cur = prev + prev2;
prev2 = prev;
prev = cur;
}
return prev;
}
The order of the three assignments matters. Compute cur from the old values first, then shift: prev2 takes the old prev, and prev takes cur. Writing prev = cur first would lose the value prev2 needs.
Time O(n), space O(1).
The same idea scales up. When a grid DP’s row i depends only on row i - 1, you keep one or two rows instead of the whole grid; when a state depends on the last k cells, you keep a window of k. You will see both in the problems that follow.
Comparing the four forms
| Form | Time | Extra space | Recursion | When to use |
|---|---|---|---|---|
| Plain recursion | O(1.618^n) |
O(n) stack |
yes | only to write down the recurrence |
| Memoization | O(n) |
O(n) table + O(n) stack |
yes | fastest to write once the recursion is right |
| Tabulation | O(n) |
O(n) table |
no | large inputs, no stack risk |
| Space optimized | O(n) |
O(1) |
no | when only a fixed window of the table is read |
In an interview, write the recursion first, because it is where the thinking happens. Memoize it, then convert to tabulation and trim the space if asked. Each step is a mechanical rewrite of the previous one, which is why the recursion has to be right.
A recipe for writing the recurrence
Most problems in this sheet yield to the same three questions:
- What is the state? Express the problem in terms of an index (or a few): “
f(i)is the answer for the firstielements” or “for the cell(i, j)”. Say precisely in words whatf(i)means. - What choices exist at that state? Try all of them: take the element or skip it, jump one step or two, come from above or from the left.
- How are the choices combined? Add them when counting ways, take the
minormaxwhen optimizing,ORthem when asking whether something is possible.
Then write the base cases: the smallest states whose answers are obvious.
Fibonacci has no real choices, but the next problems do. In Climbing Stairs, f(i) is the number of ways to reach step i, the last move was either a 1-step or a 2-step, and the counts add. In Frog Jump, f(i) is the cheapest way to reach stair i, the same two last moves exist, and you take the minimum.
Pitfalls
- Sentinel collisions. If
-1can be a genuine answer, the memo table will treat a solved state as unsolved and recompute it, which silently brings back exponential time. - Off-by-one table size. States run from
0ton, so the table needsn + 1cells. - Overflow.
F(47)already exceedsint. The methods uselong, which is exact up toF(92). Counting problems overflow quickly, so check the range of the answer before choosing a type. - Stack depth. Memoization keeps recursion depth
O(n). For largenin Java, prefer tabulation. - Vague states. If you cannot say in one sentence what
dp[i]means, the recurrence is probably wrong. Write the definition as a comment above the method. - Reusing a table across inputs. The memo table belongs to one input. Allocate it inside the public method, as the Java file does, never as a shared static field.
Related problems
- Fibonacci Number covers the same sequence from the recursion chapter, plus an
O(log n)method. - Climbing stairs is Fibonacci in disguise, and the first counting DP.
- Frog Jump is the first minimum-cost DP with the same two-step structure.
- Maximum sum of non adjacent elements introduces the pick / not-pick choice.
- Learn All Patterns of Subsequences (Theory) shows the take / not-take recursions that later DP chapters memoize.