Top-Down & Bottom-Up Dynamic Programming¶
Dynamic Programming can be implemented using two main approaches: Top-Down (memoization) and Bottom-Up (tabulation). Each approach has its advantages and is suitable for different scenarios.
Top-Down (Memoization)¶
Top-down DP starts with the main problem and recursively breaks it down into smaller subproblems, storing results to avoid recomputation.
Implementation¶
Problem: Calculate Fibonacci numbers using top-down memoization to avoid redundant calculations.
Sample Input: n = 10
Sample Output: 55 (10th Fibonacci number)
#include <unordered_map>
using namespace std;
unordered_map<int, long long> memo;
long long fibonacciMemo(int n) {
if (memo.find(n) != memo.end()) {
return memo[n];
}
if (n <= 1) {
return n;
}
memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
return memo[n];
}
Example: Longest Common Subsequence¶
Problem: Find the length of the longest common subsequence between two strings using memoization.
Sample Input:
- s1 = "ABCDGH"
- s2 = "AEDFHR"
Sample Output: 3 (LCS: "ADH")
#include <string>
#include <unordered_map>
using namespace std;
unordered_map<string, int> memo;
int lcsMemo(string s1, string s2, int i, int j) {
string key = to_string(i) + "," + to_string(j);
if (memo.find(key) != memo.end()) {
return memo[key];
}
if (i == 0 || j == 0) {
return 0;
}
int result;
if (s1[i - 1] == s2[j - 1]) {
result = 1 + lcsMemo(s1, s2, i - 1, j - 1);
} else {
result = max(lcsMemo(s1, s2, i - 1, j),
lcsMemo(s1, s2, i, j - 1));
}
memo[key] = result;
return result;
}
Bottom-Up (Tabulation)¶
Bottom-up DP solves smaller subproblems first and builds up to the main problem using iterative approaches.
Implementation¶
Problem: Calculate Fibonacci numbers using bottom-up tabulation approach.
Sample Input: n = 10
Sample Output: 55 (10th Fibonacci number)
#include <vector>
using namespace std;
long long fibonacciTab(int n) {
if (n <= 1) {
return n;
}
vector<long long> dp(n + 1, 0);
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
Example: Longest Common Subsequence¶
Problem: Find the length of the longest common subsequence between two strings using bottom-up tabulation.
Sample Input:
- s1 = "ABCDGH"
- s2 = "AEDFHR"
Sample Output: 3 (LCS: "ADH")
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int lcsTab(string s1, string s2) {
int m = s1.length(), n = s2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i - 1] == s2[j - 1]) {
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
Comparison¶
| Aspect | Top-Down | Bottom-Up |
|---|---|---|
| Approach | Recursive | Iterative |
| Memory | Only computes needed states | Computes all states |
| Stack Space | Uses recursion stack | No recursion |
| Debugging | Easier to trace | More complex |
| Space Optimization | Harder to optimize | Easier to optimize |
Space Optimization¶
Rolling Array Technique¶
Problem: Calculate Fibonacci numbers using space-optimized bottom-up approach with rolling array.
Sample Input: n = 10
Sample Output: 55 (10th Fibonacci number)
#include <vector>
using namespace std;
long long fibonacciOptimized(int n) {
if (n <= 1) {
return n;
}
long long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
long long current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
2D to 1D Optimization¶
Problem: Find the length of the longest common subsequence using space-optimized 2D to 1D approach.
Sample Input:
- s1 = "ABCDGH"
- s2 = "AEDFHR"
Sample Output: 3 (LCS: "ADH")
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int lcsOptimized(string s1, string s2) {
int m = s1.length(), n = s2.length();
if (m < n) {
swap(s1, s2);
swap(m, n);
}
vector<int> prev(n + 1, 0);
vector<int> curr(n + 1, 0);
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1[i - 1] == s2[j - 1]) {
curr[j] = 1 + prev[j - 1];
} else {
curr[j] = max(prev[j], curr[j - 1]);
}
}
prev = curr;
}
return prev[n];
}
When to Use Each Approach¶
Use Top-Down When:¶
- Problem has complex state transitions
- Not all states need to be computed
- Recursive structure is natural
- Memory is not a constraint
Use Bottom-Up When:¶
- Need to optimize space complexity
- All states must be computed
- Iterative approach is clearer
- Stack overflow is a concern
Common Patterns¶
State Transition Patterns¶
- Linear DP: States depend on previous states
- Interval DP: States represent intervals
- Tree DP: States represent tree nodes
- Bitmask DP: States use bit representations
Optimization Techniques¶
- Rolling Arrays: Reduce space from O(n) to O(1)
- Coordinate Compression: Reduce state space
- Monotonic Queue: Optimize transitions
- Convex Hull Trick: Optimize specific functions