Optimal substructure and overlapping subproblems. Memoization.
Multi-session curriculum - substantial prior knowledge and complex material. Use mastery gates and deliberate practice.
Dynamic programming (DP) is what you do when a recursive solution is “almost right”: it expresses the right logic, but it wastes time recomputing the same subproblems. DP keeps the logic and removes the waste by storing and reusing results.
Use DP when a problem has (1) optimal substructure and (2) overlapping subproblems. Define a state that uniquely identifies a subproblem, write a recurrence for its answer, and compute each state once via memoization (top-down) or tabulation (bottom-up).
This lesson assumes you already know recursion and recurrence relations. Dynamic programming builds directly on those ideas, but adds state management and complexity counting.
1) Recursion basics
2) Big-O and state-space counting (lightweight)
3) Arrays / maps / dictionaries
dp[n+1] or dp[n+1][m+1].4) Indexing and 2D tables
dp[i] and dp[i][j], and iterate in nested loops.If any of these feel shaky, you can still proceed, but expect to pause and practice implementing small DP tables carefully.
Dynamic programming is a technique for solving problems by:
1) Breaking the problem into subproblems
2) Ensuring each subproblem is solved once
3) Combining subproblem solutions to solve larger problems
DP is not “one algorithm.” It’s a pattern.
Many problems have a natural recursive definition. For example, Fibonacci numbers:
A direct recursive implementation mirrors the math, but it recomputes the same values repeatedly.
If you draw the recursion tree for , you’ll notice the same subcalls appear many times (like , ). That repeated work is the key inefficiency.
The recursion generates the same subproblems again and again.
An optimal solution to the whole problem can be formed from optimal solutions to its subproblems.
DP typically appears in optimization (min/max) problems, but it also applies to counting (number of ways), decision (is it possible?), and probability.
We store the answer to a subproblem in a table:
dp[state] = the computed result for that subproblemThe entire art of DP often reduces to: choose a good state and write a correct recurrence.
| Approach | Idea | Pros | Cons |
|---|---|---|---|
| Top-down (memoization) | Write recursion; cache results | Closest to the recursive logic; computes only needed states | Stack depth risk; overhead from recursion |
| Bottom-up (tabulation) | Fill dp table iteratively | No recursion; often faster in practice | Must find correct iteration order; may compute unused states |
You should be able to do both. Real interviews and real systems often benefit from being fluent in switching between them.
DP begins with a precise question:
What information do I need to uniquely identify a subproblem?
That “information” is your state.
A good DP state is:
If your state is missing information, your recurrence will “pretend” two different situations are the same. That produces wrong answers.
Subproblem: compute .
State: just .
So dp[n] is enough.
Subproblem: number of ways to reach cell .
State: .
So dp[i][j].
Subproblem: best value using first items with capacity .
State: .
So dp[i][w].
Once you define state, you can estimate complexity by counting how many states exist.
dp[n]: you have states → often time if each is .dp[i][w] with and : you have states → often time.This is essential: DP often replaces exponential recursion with polynomial-time via “compute each state once.”
If you include unnecessary information in state, the number of states can explode.
Example: using the entire partial solution history as part of state makes DP infeasible.
If you leave out necessary information, you merge distinct subproblems.
Example idea: Suppose a path problem where whether you can step on a cell depends on whether you already used a “skip” power-up. Then state must include usedSkip ∈ {0,1}; dp[i][j] alone is too small.
When you’re stuck:
1) Write the recursive function signature you wish you had.
2) The arguments to that function are usually your state.
3) Then ask: can I memoize those arguments?
Once you have a state, you need a recurrence: how do you compute dp[state] from smaller states?
If you don’t know what the subproblem is, you can’t correctly express how it relates to smaller subproblems.
You write a recursive function solve(state):
1) If state is a base case, return base answer.
2) If answer is already memoized, return it.
3) Otherwise, compute answer by trying choices that reduce the problem.
4) Store in dp[state], return it.
Pseudo-pattern:
solve(state):
if base(state): return baseValue
if dp has state: return dp[state]
ans = combine( solve(nextState1), solve(nextState2), ... )
dp[state] = ans
return ansThe recurrence is:
A memoized version computes each once, turning exponential recursion into linear time.
Complexity:
For large (like ), recursion may overflow. That’s a reason to prefer bottom-up.
grows roughly like , so it exceeds 32-bit quickly (e.g., > 2³¹−1). Use 64-bit (long long) or big integers, or compute modulo.
| State type | Good memo structure | Notes |
|---|---|---|
| Single integer (n) | array/vector | fastest and simplest |
| Pair (i,j) bounded | 2D array | memory may be large: (n·m) |
| Complex/unbounded | hash map keyed by tuple/string | slower but flexible |
Memoization is conceptually simple, but it only works if your state uniquely identifies subproblems.
Bottom-up DP computes answers for small states first, then builds up to the target.
Your recurrence defines dependencies. You must fill the table so that when computing dp[state], all required smaller states are already computed.
We can compute:
dp[0]=0dp[1]=1i=2..n: dp[i]=dp[i-1]+dp[i-2]This order works because dp[i] depends only on earlier indices.
If dp[i][j] depends on dp[i-1][j] and dp[i][j-1], then scanning rows top-to-bottom and columns left-to-right works.
Often you don’t need the whole table.
Since dp[i] depends only on last two values:
prev2 = dp[i-2], prev1 = dp[i-1].If state uses dimension i and only depends on i-1, you can compress:
dp[i][w] to 1D dp[w] (careful with iteration direction!)For 0/1 knapsack, if you compress dp[i][w] → dp[w], you must iterate w descending to avoid reusing item i multiple times.
This is a classic DP bug: same recurrence, wrong loop order, wrong meaning.
DP isn’t just a coding trick; it’s a general method for sequential decision problems.
In many problems, dp[state] represents the best achievable value from that state onward.
That’s exactly the mindset used in Markov Decision Processes (MDPs): define a value function over states.
In optimization DP, a common form is:
or for maximizing rewards:
This is a Bellman equation idea: the value of a state equals the best immediate choice plus the value of the next state.
MDPs generalize DP to settings with uncertainty and expectations. Instead of next(s,a) being a single next state, it’s a distribution, and you compute expected value:
If you understand:
then Bellman equations feel like the same pattern, just with probabilities.
DP appears in:
The recurring workflow is:
1) model as states
2) define transitions/choices
3) compute values efficiently
4) optionally reconstruct the solution path
Compute F(n) with F(0)=0, F(1)=1. Show why naive recursion is slow and how memoization/tabulation fix it.
Start from the recurrence:
with base cases , .
Naive recursion (conceptually) calls:
So F(n-2) is computed multiple times.
This repeated computation grows rapidly, leading to about time.
Define the DP state:
dp[n] store Top-down memoization derivation:
dp[n] already filled, return itStore and return.
Complexity by state counting:
So time is and space is (plus recursion stack).
Bottom-up tabulation:
Initialize:
dp[0]=0dp[1]=1Then for i=2..n:
Space optimization:
Because dp[i] uses only dp[i-1], dp[i-2], keep two variables:
a = F(i-2)b = F(i-1)Update c=a+b, then shift.
Insight: DP doesn’t change the math recurrence. It changes the execution strategy: compute each subproblem once by caching (memoization) or ordering (tabulation).
You have an array cost[0..n-1]. You can start at step 0 or 1. To reach the top (step n), you pay the cost of each step you land on, and you can climb 1 or 2 steps at a time. Find the minimum total cost.
Why DP?
A recursive solution tries both move sizes at each step (1 or 2), which creates repeated subcalls for the same step index → overlapping subproblems.
Also the optimal way to reach the top from step i uses optimal ways from i+1 and i+2 → optimal substructure.
Define state:
Let dp[i] = minimum cost to reach step i (where i can be 0..n).
Interpretation detail:
Set base cases carefully:
You can start at 0 or 1 without paying anything yet (you pay when you land on a step).
A common clean setup:
dp[0]=0dp[1]=0Write the recurrence.
To arrive at step i (for i ≥ 2), you came from i-1 or i-2.
If you came from i-1, you must have paid cost[i-1] when stepping on i-1.
If you came from i-2, you must have paid cost[i-2] when stepping on i-2.
So:
Compute bottom-up:
For i = 2..n:
Return dp[n].
Complexity:
So time O(n), space O(n), and space can be optimized to O(1) using two variables.
Insight: Good DP often comes from defining dp[i] as the best cost to reach position i, then carefully accounting for what cost is paid on the previous step. Most bugs are off-by-one or misinterpreting when costs apply.
Given n items, item i has weight wᵢ and value vᵢ. Capacity is W. Choose a subset (each item at most once) to maximize total value without exceeding W.
Why DP?
A brute-force search over subsets is 2ⁿ. But the problem has optimal substructure: an optimal solution using first i items and capacity w depends on optimal solutions with first i-1 items.
It also has overlapping subproblems because many subsets lead to the same (i, w) situation.
Define state:
Let dp[i][w] = maximum value achievable using items 1..i with capacity w.
State is (i, w). This uniquely identifies the subproblem.
Base cases:
dp[0][w]=0 for all w (no items, no value)dp[i][0]=0 for all i (zero capacity, no value)Recurrence (show both choices).
For item i (weight wᵢ, value vᵢ):
So:
Tabulation order:
Compute i from 1..n, and for each i compute w from 0..W.
This ensures dp[i-1][] is ready before dp[i][].
Complexity:
So time O(nW), space O(nW).
Reconstruction (what items were chosen):
After filling dp, start at (i=n, w=W) and walk backward:
This is a backpointer-by-comparison technique; alternatively store explicit choice[i][w].
Insight: Knapsack shows the full DP workflow: define a 2D state, write a max recurrence over choices, fill the table in dependency order, and optionally reconstruct the actual optimal subset with backtracking.
Dynamic programming applies when you have overlapping subproblems and optimal substructure.
The hardest (and most important) step is defining the state: the minimal information that uniquely identifies a subproblem.
dp[state] stores the answer for that state; DP’s speed comes from computing each state once.
Top-down DP (memoization) keeps recursive structure but can hit recursion depth limits; bottom-up DP (tabulation) avoids recursion and needs a correct fill order.
Estimate DP time/space by counting states × work per state.
Loop order and indexing define meaning; the same recurrence can become wrong if you fill in the wrong direction (especially in compressed knapsack).
Many DP problems also need reconstruction; plan to store backpointers or enable backward tracing through the dp table.
DP is the algorithmic foundation for Bellman equations and helps prepare you for MDPs and reinforcement learning value functions.
Choosing a state that is missing a crucial variable (merging distinct subproblems and producing incorrect results).
Off-by-one errors in base cases and table size (e.g., confusing dp over indices 0..n-1 vs 0..n).
Using top-down recursion for very large depths and crashing due to stack overflow; switching to bottom-up fixes it.
Forgetting numeric limits (integer overflow) or failing to store enough info to reconstruct the optimal solution (no backpointers / no traceback plan).
Compute the number of distinct ways to climb n stairs if you can climb 1 or 2 steps at a time. Return the answer for n (assume n ≥ 0). Define a DP state and recurrence, and give the time and space complexity.
Hint: Let dp[i] be the number of ways to reach step i. Think about dp[i-1] and dp[i-2]. Be careful with dp[0].
State: dp[i] = # ways to reach step i.
Base: dp[0]=1 (one way to be at the start), dp[1]=1.
Recurrence for i≥2:
Answer: dp[n].
Complexity: O(n) time, O(n) space (or O(1) with two variables).
Given a 2D grid of nonnegative costs cost[i][j], find the minimum cost path from (0,0) to (n-1,m-1) moving only right or down. Write the DP recurrence and describe a valid fill order.
Hint: Let dp[i][j] be the minimum cost to reach (i,j). Handle first row/column separately or via sentinel values.
State: dp[i][j] = min cost to reach cell (i,j).
Base: dp[0][0] = cost[0][0].
For i>0, j>0:
First row: dp[0][j] = cost[0][j] + dp[0][j-1].
First column: dp[i][0] = cost[i][0] + dp[i-1][0].
Fill order: i from 0..n-1, j from 0..m-1 (row-major) works because dependencies are top and left.
0/1 Knapsack space optimization: Starting from dp[i][w], compress to 1D dp[w]. Explain why iterating w from W down to wᵢ is necessary, and give the 1D recurrence.
Hint: If you iterate w upward, dp[w-wᵢ] may already include item i, turning 0/1 knapsack into unbounded knapsack.
Use dp[w] = best value with capacity w using items processed so far.
For each item i, update:
For w = W down to wᵢ:
Descending order ensures dp[w-wᵢ] is from the previous iteration of i (i-1 items), so item i is used at most once. Ascending would allow reusing item i multiple times because dp[w-wᵢ] might have already been updated with item i.
Next, DP becomes a framework for sequential decision making via Bellman equations:
Related algorithmic patterns you’ll likely connect after DP: