Dynamic Programming I: Foundations
DP ভিত্তি — মেমোইজেশন ও tabulation
1. What Makes a Problem "DP-able"?
- Overlapping subproblems: the same sub-question is asked many times during a naive recursion.
- Optimal substructure: the optimal answer to the whole composes from optimal answers to parts.
If both hold, you can compute each subproblem once, store the answer, and reuse — turning exponential brute force into polynomial time. This is dynamic programming.
2. Top-Down (Memoise) vs Bottom-Up (Tabulate)
| Style | Top-down (memoise) | Bottom-up (tabulate) |
|---|---|---|
| How it runs | Recursion + cache | Iterative loops |
| Order | Demand-driven | Explicit topological order |
| Memory | Cache size + stack | DP table only |
| Easier to space-optimise? | Harder | Easier (rolling array) |
| Best for | Sparse states / hard order | Dense states / hot loops |
3. The Same Problem in 4 Forms
Fibonacci — naive O(2ⁿ), memoised O(n), tabulated O(n), and O(1) space. Same recurrence, four implementations, dramatic speedup.
#include <bits/stdc++.h>
using namespace std;
// 1) Naive — exponential
long long fib1(int n) {
if (n < 2) return n;
return fib1(n-1) + fib1(n-2);
}
// 2) Memoised top-down — O(n) time, O(n) space
long long memo[100];
bool seen[100];
long long fib2(int n) {
if (n < 2) return n;
if (seen[n]) return memo[n];
seen[n] = true;
return memo[n] = fib2(n-1) + fib2(n-2);
}
// 3) Tabulated bottom-up — O(n) time, O(n) space
long long fib3(int n) {
vector<long long> dp(n+1);
dp[0] = 0; if (n >= 1) dp[1] = 1;
for (int i = 2; i <= n; i++) dp[i] = dp[i-1] + dp[i-2];
return dp[n];
}
// 4) O(1) space — rolling two variables
long long fib4(int n) {
long long a = 0, b = 1;
for (int i = 0; i < n; i++) { long long t = a + b; a = b; b = t; }
return a;
}
int main() {
cout << "fib(30) naive = " << fib1(30) << "\n";
cout << "fib(50) memoise = " << fib2(50) << "\n";
cout << "fib(50) tabulate = " << fib3(50) << "\n";
cout << "fib(50) O(1) sp = " << fib4(50);
}
4. State Design — The Hardest Part
For each DP, decide:
- State: what do you need to remember at each step? Often
dp[i]means "answer if I am at position i". - Transition: how is dp[i] computed from earlier states?
- Base case: the smallest input where the answer is direct.
Once these three are written, implementation is mechanical. Bad state design is the most common reason DP problems get stuck.
5. Climbing Stairs & House Robber — Two Quick DPs
#include <bits/stdc++.h>
using namespace std;
// climbStairs(n): ways to reach step n by 1 or 2 steps
int climbStairs(int n) {
int a = 1, b = 1;
for (int i = 2; i <= n; i++) { int t = a + b; a = b; b = t; }
return b;
}
// rob(houses): max money — can't rob adjacent
int rob(vector<int>& h) {
int prev = 0, cur = 0;
for (int x : h) { int t = max(cur, prev + x); prev = cur; cur = t; }
return cur;
}
int main() {
cout << "climbStairs(5) = " << climbStairs(5) << "\n";
vector<int> h = {2, 7, 9, 3, 1};
cout << "rob = " << rob(h);
}
6. Practice Problems
-
House Robber II — houses arranged in a circle.House Robber II — circular array।
✨ Show Answer (উত্তর দেখুন)
Approach: run rob() on h[0..n−2] and on h[1..n−1] separately. Return the maximum.
-
Decode Ways — count how many ways to decode "12345" given A=1, B=2, …, Z=26.Decode Ways।
✨ Show Answer (উত্তর দেখুন)
Approach: dp[i] = ways to decode s[0..i]. dp[i] = dp[i−1] (if s[i] is 1..9) + dp[i−2] (if s[i−1..i] is 10..26).
-
Unique Paths in m × n grid (only right or down moves).Grid-এ unique path।
✨ Show Answer (উত্তর দেখুন)
Approach: dp[i][j] = dp[i−1][j] + dp[i][j−1] with dp[0][0] = 1. Or use combinatorics: C(m+n−2, m−1).
-
Min Path Sum from top-left to bottom-right of a non-negative grid.Min path sum।
✨ Show Answer (উত্তর দেখুন)
Approach: dp[i][j] = grid[i][j] + min(dp[i−1][j], dp[i][j−1]) with care for the first row/column. O(mn) time, O(n) space.
-
Tribonacci numbers T(n) = T(n−1) + T(n−2) + T(n−3).Tribonacci।
✨ Show Answer (উত্তর দেখুন)
Approach: rolling three variables a, b, c.
t = a + b + c; a = b; b = c; c = t; -
Perfect Squares — minimum count of perfect squares summing to n.Perfect squares — minimum count।
✨ Show Answer (উত্তর দেখুন)
Approach: dp[i] = 1 + min over k where k² ≤ i of dp[i − k²]. O(n √n).
-
Min cost climbing stairs with cost array — pay cost[i] then move 1 or 2 steps.Min cost climbing stairs।
✨ Show Answer (উত্তর দেখুন)
Approach: dp[i] = cost[i] + min(dp[i−1], dp[i−2]). Answer = min(dp[n−1], dp[n−2]).
-
Why does memoisation give O(n) time for fib but naive recursion is O(2ⁿ)?Memoisation কেন এতটা দ্রুত?
✨ Show Answer (উত্তর দেখুন)
Answer: there are only n distinct subproblems (fib(0), fib(1), …, fib(n)). Each is computed once and cached, then reused. Naive recursion solves fib(k) ~Fₙ₋ₖ times → exponential.
Summary — Module 34
DP applies when subproblems overlap and optimum composes. Choose memoise for sparse states, tabulate for dense. Always check whether memory can collapse to O(1) (rolling). The hardest part is state design — three sentences (state / transition / base) discipline you.