Dynamic Programming
Dynamic Programming (DP) is a way to solve problems where:
- The same subproblems repeat many times, and
- The answer to the big problem can be built from answers to smaller subproblems.
Key idea: Don’t recompute the same thing over and over. Instead, remember (memoize) results in an array or map and reuse them.
Warm-Up: Fibonacci Numbers
We define the Fibonacci sequence:
F(0) = 0F(1) = 1F(n) = F(n-1) + F(n-2)forn ≥ 2
Naive Recursive Solution (Slow)
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}What’s wrong with this?
fib(5)callsfib(4)andfib(3)fib(4)callsfib(3)andfib(2)fib(3)gets called again and again
This creates an exponential number of calls: Time ≈ O(2^n) — too slow for large n.
Fixing It with Memoization (Top-Down DP)
We create an array dp[] where:
dp[n]= the value offib(n)once computed.
Before computing fib(n), we check if it was already computed.
#include <iostream>
#include <vector>
using namespace std;
vector<long long> dp;
long long fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
if (dp[n] != -1) return dp[n]; // already computed
dp[n] = fib(n - 1) + fib(n - 2);
return dp[n];
}
int main() {
int n = 50;
dp.assign(n + 1, -1);
cout << "F(" << n << ") = " << fib(n) << "\n";
}Now each fib(k) is computed once. All later calls just reuse the stored answer.
SRTBOT Framework for DP
Let’s use SRTBOT to organize our DP thinking:
S – State
What does dp[i] represent?
dp[i]=F(i), thei-th Fibonacci number.
R – Recurrence (Relation)
How do we build big states from smaller ones?
dp[i] = dp[i-1] + dp[i-2]fori ≥ 2
T – Transitions (How we move between states)
To compute dp[i], we use:
dp[i-1]dp[i-2]
B – Base Cases
We must define the first values:
dp[0] = 0dp[1] = 1
O – Order of Computation
For bottom-up DP:
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}We compute from small to large (0 → 1 → 2 → ... → n).
T – Time & Space Complexity
- Time: each
ifrom 0 tonis computed once →O(n) - Space: we store
dp[0..n]→O(n)(can be reduced toO(1)by only keeping the last two values)
Minimum Cost to Reach the End
Problem
You are given an array cost[0..n-1], where cost[i] is the energy or money you pay when you step on position i.
You start before the array and want to go beyond the last index (to position n). From index i, you may move to:
i + 1, ori + 2
You can start from index 0 or index 1.
Goal: Find the minimum total cost to reach position n.
Example
cost = [1, 100, 1, 1]
Possible ways:
Start at 0:
0 -> 2 -> n
cost = cost[0] + cost[2] = 1 + 1 = 2
Start at 1:
1 -> 3 -> n
cost = 100 + 1 = 101
Answer = 2
Naive Recursive Solution
Let’s define a recursive function:
solve(i)= minimum cost to go from indexito the end (positionn).
Transitions:
- From
i, you paycost[i], then go toi+1ori+2.
int n;
vector<int> cost;
int solve(int i) {
if (i >= n) return 0;
int take1 = solve(i + 1);
int take2 = solve(i + 2);
return cost[i] + min(take1, take2);
}
int main() {
cost = {1, 100, 1, 1};
n = cost.size();
int ans = min(solve(0), solve(1)); // can start from 0 or 1
cout << ans << "\n";
}Why is this slow?
solve(i)callssolve(i+1)andsolve(i+2).- Those calls overlap heavily (same
irecomputed many times). - Time becomes exponential in
n(O(2^n)).
Add Memoization (Top-Down DP)
We store the answer for each i in dp[i]:
#include <iostream>
#include <vector>
using namespace std;
int n;
vector<int> cost;
vector<int> dp;
int solve(int i) {
if (i >= n) return 0;
if (dp[i] != -1) return dp[i]; // already computed
int take1 = solve(i + 1);
int take2 = solve(i + 2);
dp[i] = cost[i] + min(take1, take2);
return dp[i];
}
int main() {
cost = {1, 100, 1, 1};
n = cost.size();
dp.assign(n, -1);
int ans = min(solve(0), solve(1));
cout << "Minimum cost = " << ans << "\n";
}Now:
- Each
solve(i)is computed once. - Total time is O(n) instead of exponential.
Bottom-Up DP Version
We can also do it iteratively from the end to the beginning.
Define:
dp[i]= minimum cost to go from indexito the end.
Then:
If
i >= n, cost is0.For
i < n:dp[i] = cost[i] + min(dp[i+1], dp[i+2])
We usually make the array slightly larger (size n+2) to avoid bounds checks.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> cost = {1, 100, 1, 1};
int n = cost.size();
vector<int> dp(n + 2, 0); // dp[n] and dp[n+1] = 0 by default
for (int i = n - 1; i >= 0; i--) {
dp[i] = cost[i] + min(dp[i + 1], dp[i + 2]);
}
int ans = min(dp[0], dp[1]); // can start from index 0 or 1
cout << "Minimum cost = " << ans << "\n";
}SRTBOT for Minimum Cost to Reach the End
Let’s apply the SRTBOT framework.
S – State
dp[i]= minimum total cost to go from indexito positionn(i.e., to reach or go beyond the end).
R – Recurrence
From index i, you:
- Pay
cost[i] - Then jump to
i+1ori+2
So:
dp[i] = cost[i] + min(dp[i+1], dp[i+2])
T – Transitions
From state i, we can transition to:
i + 1i + 2
Those are the only “edges” in our DP graph.
B – Base Cases
We define:
dp[n] = 0(already at/beyond the end, no more cost)dp[n+1] = 0(safe padding to usedp[i+2]wheni = n-1)
In top-down:
- If
i >= n, return0.
O – Order of Computation
For bottom-up:
We process
ifromn-1down to0, so that:dp[i+1]anddp[i+2]are already computed when we calculatedp[i].
for (int i = n - 1; i >= 0; i--) {
dp[i] = cost[i] + min(dp[i + 1], dp[i + 2]);
}T – Time & Space Complexity
Time: We compute
dp[i]once for eachiin[0..n-1], each in O(1) → total O(n).Space: We use an array
dpof sizen+2→ O(n). (We can even reduce it to O(1) by only keeping the last two values.)
Big Picture Intuition for DP
Dynamic Programming usually appears when:
You can write a recursive solution that:
- Combines answers to smaller subproblems
- But recomputes the same subproblems many times
You then:
- Define a state (
dp[...]) - Derive a *ecurrence
- Add memoization (top-down) or fill a table (bottom-up)
- Define a state (
SRTBOT helps you structure your thinking:
State → Recurrence → Transitions → Base cases → Order of computation → Time/space complexity