Recursion To Dp
Last updated: Apr 3, 2026Table of Contents
- 0) Concept
- 0-0) When to Convert Recursion to DP
- 0-1) Conversion Strategy
- 1) Complete Examples: Recursion → DP
- 1-1) Fibonacci Sequence
- 1-2) Climbing Stairs (LC 70)
- 1-3) House Robber (LC 198)
- 1-4) Coin Change (LC 322)
- 2) Common LeetCode Problems
- Recursion → DP Conversions
- 3) Conversion Checklist
- ✅ Step-by-Step Guide
- 4) Interview Tips
- 💡 Recognition Patterns
- 🎯 Interview Talking Points
- 📊 Complexity Analysis
- Summary
- LC Examples
- 2-1) House Robber (LC 198) — Recursion → Memoization → DP
- 2-2) Word Break (LC 139) — Top-down Recursion → DP
- 2-3) Edit Distance (LC 72) — 2D DP (String → String)
- 2-4) Fibonacci Number (LC 509) — Recursion → Memoization → DP → O(1)
- 2-5) Unique Paths (LC 62) — 2D Recursion → DP
- 2-6) Triangle (LC 120) — Top-Down Recursion → Bottom-Up DP
- 2-7) Longest Palindromic Subsequence (LC 516) — Interval DP
- 2-8) Coin Change (LC 322) — Recursion with Pruning → DP
- 2-9) Combination Sum IV (LC 377) — Recursion → DP (Order Matters)
- 2-10) Minimum Cost to Cut a Stick (LC 1547) — Interval DP
- 2-11) Partition Array for Maximum Sum (LC 1043) — 1D DP
Recursion to Dynamic Programming Conversion
- Core idea: Transform recursive solutions into iterative DP for better performance
- When to use: Recursive solutions with overlapping subproblems and optimal substructure
- Key benefits: Eliminate redundant calculations, reduce space from O(n) stack to O(n) or O(1) array
- Common pattern: Recognize memoization opportunities, then convert to bottom-up tabulation
Conversion Steps:
- Identify base cases
- Find recursive relation
- Add memoization (Top-Down DP)
- Convert to tabulation (Bottom-Up DP)
- Optimize space if possible
0) Concept
0-0) When to Convert Recursion to DP
Indicators that DP is applicable:
“When you hear a problem beginning with the following statements, it’s often (though not always) a good candidate for recursion: ‘Design an algorithm to compute the nth…’, ‘Write code to list the first n…’, ‘Implement a method to compute all…’, and so on.” — Cracking the Coding Interview, 6th Edition, p.130
Requirements for DP:
- Overlapping Subproblems: Same subproblems solved multiple times
- Optimal Substructure: Optimal solution contains optimal solutions to subproblems
- Memoization Opportunity: Results can be cached for reuse
Recognition Patterns:
- “Find the nth…”
- “Count ways to…”
- “Minimum/maximum…”
- “Optimize…”
- Multiple recursive calls with same parameters
0-1) Conversion Strategy
Recursion (Exponential)
↓
Top-Down DP (Memoization)
↓
Bottom-Up DP (Tabulation)
↓
Space-Optimized DP
Top-Down (Memoization):
- Keep recursive structure
- Add cache (memo) to store results
- Easy to implement from recursion
- Space: O(n) for memo + O(n) for call stack
Bottom-Up (Tabulation):
- Build solution iteratively
- Fill DP table from base cases up
- No recursion overhead
- Space: O(n) for DP table only
Space Optimization:
- Identify what previous states are actually needed
- Often can reduce from O(n) to O(k) where k is constant
- Example: Fibonacci only needs last 2 values
1) Complete Examples: Recursion → DP
1-1) Fibonacci Sequence
Problem: Compute the nth Fibonacci number where F(n) = F(n-1) + F(n-2), F(0)=0, F(1)=1.
Step 1: Naive Recursion
# Python - Naive Recursion
def fib_recursive(n):
"""
Time: O(2^n) - exponential
Space: O(n) - call stack depth
"""
if n <= 1:
return n
return fib_recursive(n-1) + fib_recursive(n-2)
// Java - Naive Recursion
public int fibRecursive(int n) {
/**
* time = O(2^N) - exponential
* space = O(N) - call stack
*/
if (n <= 1) return n;
return fibRecursive(n-1) + fibRecursive(n-2);
}
Problem: Massive redundant calculations!
fib(5)
├── fib(4)
│ ├── fib(3)
│ │ ├── fib(2)
│ │ │ ├── fib(1)
│ │ │ └── fib(0)
│ │ └── fib(1)
│ └── fib(2) <- Computed again!
│ ├── fib(1)
│ └── fib(0)
└── fib(3) <- Computed again!
├── fib(2) <- Computed again!
│ ├── fib(1)
│ └── fib(0)
└── fib(1)
Step 2: Top-Down DP (Memoization)
# Python - Top-Down DP
def fib_memo(n, memo=None):
"""
Time: O(n) - each subproblem solved once
Space: O(n) - memo dict + call stack
"""
if memo is None:
memo = {}
if n <= 1:
return n
if n in memo:
return memo[n]
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
// Java - Top-Down DP
public int fibMemo(int n) {
return fibMemoHelper(n, new int[n+1]);
}
private int fibMemoHelper(int n, int[] memo) {
/**
* time = O(N)
* space = O(N) - memo + call stack
*/
if (n <= 1) return n;
if (memo[n] != 0) return memo[n];
memo[n] = fibMemoHelper(n-1, memo) + fibMemoHelper(n-2, memo);
return memo[n];
}
Step 3: Bottom-Up DP (Tabulation)
# Python - Bottom-Up DP
def fib_dp(n):
"""
Time: O(n)
Space: O(n) - DP table
"""
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
// Java - Bottom-Up DP
public int fibDP(int n) {
/**
* time = O(N)
* space = O(N)
*/
if (n <= 1) return n;
int[] dp = new int[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];
}
Step 4: Space-Optimized DP
# Python - Space Optimized
def fib_optimized(n):
"""
Time: O(n)
Space: O(1) - only 2 variables
"""
if n <= 1:
return n
prev2, prev1 = 0, 1
for i in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1
// Java - Space Optimized
public int fibOptimized(int n) {
/**
* time = O(N)
* space = O(1)
*/
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
Summary:
| Approach | Time | Space | Notes |
|---|---|---|---|
| Naive Recursion | O(2^n) | O(n) | Exponential, unusable for n>40 |
| Memoization | O(n) | O(n) + O(n) | Easy to code, still has stack overhead |
| Tabulation | O(n) | O(n) | No recursion, cleaner |
| Space Optimized | O(n) | O(1) | Best overall |
1-2) Climbing Stairs (LC 70)
Problem: Climbing n stairs, can climb 1 or 2 steps. How many distinct ways?
Step 1: Recursion
def climbStairs_recursive(n):
"""Time: O(2^n), Space: O(n)"""
if n <= 2:
return n
return climbStairs_recursive(n-1) + climbStairs_recursive(n-2)
Step 2: Memoization
def climbStairs_memo(n, memo=None):
"""Time: O(n), Space: O(n)"""
if memo is None:
memo = {}
if n <= 2:
return n
if n in memo:
return memo[n]
memo[n] = climbStairs_memo(n-1, memo) + climbStairs_memo(n-2, memo)
return memo[n]
Step 3: Bottom-Up DP
// LC 70 - Climbing Stairs
public int climbStairs(int n) {
/**
* time = O(N)
* space = O(N)
*/
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
Step 4: Space Optimized
public int climbStairsOptimized(int n) {
/**
* time = O(N)
* space = O(1)
*/
if (n <= 2) return n;
int prev2 = 1, prev1 = 2;
for (int i = 3; i <= n; i++) {
int current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
1-3) House Robber (LC 198)
Problem: Array of house values. Can’t rob adjacent houses. Maximum robbery amount?
Step 1: Recursion
def rob_recursive(nums, i=0):
"""
Time: O(2^n)
Space: O(n)
"""
if i >= len(nums):
return 0
# Choice: rob current house or skip
rob_current = nums[i] + rob_recursive(nums, i+2)
skip_current = rob_recursive(nums, i+1)
return max(rob_current, skip_current)
Step 2: Memoization
def rob_memo(nums, i=0, memo=None):
"""Time: O(n), Space: O(n)"""
if memo is None:
memo = {}
if i >= len(nums):
return 0
if i in memo:
return memo[i]
rob_current = nums[i] + rob_memo(nums, i+2, memo)
skip_current = rob_memo(nums, i+1, memo)
memo[i] = max(rob_current, skip_current)
return memo[i]
Step 3: Bottom-Up DP
// LC 198 - House Robber
public int rob(int[] nums) {
/**
* time = O(N)
* space = O(N)
*/
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
// Choice: rob current + dp[i-2] OR skip current (dp[i-1])
dp[i] = Math.max(nums[i] + dp[i-2], dp[i-1]);
}
return dp[nums.length - 1];
}
Step 4: Space Optimized
public int robOptimized(int[] nums) {
/**
* time = O(N)
* space = O(1)
*/
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int prev2 = nums[0];
int prev1 = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
int current = Math.max(nums[i] + prev2, prev1);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
1-4) Coin Change (LC 322)
Problem: Array of coin denominations, target amount. Minimum coins needed?
Step 1: Recursion
def coinChange_recursive(coins, amount):
"""
Time: O(S^n) where S = amount, n = coins
Space: O(amount)
"""
if amount == 0:
return 0
if amount < 0:
return -1
min_coins = float('inf')
for coin in coins:
result = coinChange_recursive(coins, amount - coin)
if result >= 0:
min_coins = min(min_coins, result + 1)
return min_coins if min_coins != float('inf') else -1
Step 2: Memoization
def coinChange_memo(coins, amount, memo=None):
"""Time: O(S × n), Space: O(S)"""
if memo is None:
memo = {}
if amount == 0:
return 0
if amount < 0:
return -1
if amount in memo:
return memo[amount]
min_coins = float('inf')
for coin in coins:
result = coinChange_memo(coins, amount - coin, memo)
if result >= 0:
min_coins = min(min_coins, result + 1)
memo[amount] = min_coins if min_coins != float('inf') else -1
return memo[amount]
Step 3: Bottom-Up DP
// LC 322 - Coin Change
public int coinChange(int[] coins, int amount) {
/**
* time = O(S × N) where S = amount, N = coins
* space = O(S)
*/
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // Infinity placeholder
dp[0] = 0; // Base case
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
2) Common LeetCode Problems
Recursion → DP Conversions
| Problem | Difficulty | Recursive Pattern | DP Type |
|---|---|---|---|
| LC 70 | Easy | Stairs = (n-1) + (n-2) | 1D DP |
| LC 198 | Medium | Rob = max(rob, skip) | 1D DP |
| LC 322 | Medium | Min coins for amount | Unbounded knapsack |
| LC 509 | Easy | Fibonacci | Classic |
| LC 746 | Easy | Min cost climbing | 1D DP |
| LC 139 | Medium | Word break | String DP |
| LC 300 | Medium | Longest increasing | Subsequence DP |
| LC 416 | Medium | Partition subset | 0/1 knapsack |
3) Conversion Checklist
✅ Step-by-Step Guide
-
Identify Base Cases
- What are the simplest inputs?
- What can be returned immediately?
-
Find Recursive Relation
- How does F(n) relate to F(n-1), F(n-2), etc.?
- What choices/decisions are made at each step?
-
Add Memoization (Top-Down)
- Create cache/memo structure
- Check cache before computing
- Store result after computing
-
Convert to Tabulation (Bottom-Up)
- Create DP array
- Fill base cases
- Iterate from small to large subproblems
- Use recurrence relation to fill table
-
Optimize Space
- Identify which previous states are needed
- Use variables instead of full array if possible
4) Interview Tips
💡 Recognition Patterns
When to suspect DP:
- “Find the nth…”
- “Count ways to…”
- “Minimum/maximum…”
- Multiple recursive calls
- Overlapping subproblems
Conversion Strategy:
- Start with recursion (easier to understand)
- Add memoization (quick win)
- Convert to bottom-up if asked
- Optimize space if time permits
🎯 Interview Talking Points
-
Why DP is better:
- “Eliminates redundant calculations”
- “Trades space for time”
- “Linear time instead of exponential”
-
Top-Down vs Bottom-Up:
- “Top-down is easier to code from recursion”
- “Bottom-up is more efficient (no stack overhead)”
- “Both have same time complexity”
-
Space Optimization:
- “Only need previous k states”
- “Can reduce from O(n) to O(k)”
- “Common for 1D DP problems”
📊 Complexity Analysis
| Approach | Typical Time | Typical Space | Notes |
|---|---|---|---|
| Naive Recursion | O(2^n) | O(n) | Unusable for n>30 |
| Memoization | O(n) | O(n) + O(n) | Easy to code |
| Tabulation | O(n) | O(n) | More efficient |
| Space Optimized | O(n) | O(1) or O(k) | Best overall |
Summary
Core Principles:
- ✅ Recursion → Memoization → Tabulation → Space Optimization
- ✅ Look for overlapping subproblems
- ✅ Memoization keeps recursive structure, adds cache
- ✅ Tabulation builds solution iteratively from base cases
When to Use:
- “Find the nth…” problems
- Optimization problems (min/max)
- Counting problems (number of ways)
- Problems with choices at each step
Interview Strategy:
- Start with recursive solution
- Identify overlapping subproblems
- Add memoization
- Convert to bottom-up if needed
- Optimize space if possible
Key Insight: Every DP problem can be solved recursively first, then optimized with memoization/tabulation.
LC Examples
2-1) House Robber (LC 198) — Recursion → Memoization → DP
Cannot rob two adjacent houses; dp[i] = max(dp[i-1], dp[i-2] + nums[i]).
// LC 198 - House Robber
// IDEA: DP — dp[i] = max money robbing up to house i
// time = O(N), space = O(1)
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int prev2 = nums[0], prev1 = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
int curr = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
2-2) Word Break (LC 139) — Top-down Recursion → DP
dp[i] = true if s[0…i] can be segmented using wordDict.
// LC 139 - Word Break
// IDEA: DP — dp[i] means s[0..i-1] can be segmented
// time = O(N^2), space = O(N)
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> dict = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
if (dp[j] && dict.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
return dp[n];
}
2-3) Edit Distance (LC 72) — 2D DP (String → String)
dp[i][j] = min operations to convert s1[0…i] to s2[0…j].
// LC 72 - Edit Distance
// IDEA: 2D DP — dp[i][j] = min ops to convert word1[0..i-1] to word2[0..j-1]
// time = O(M*N), space = O(M*N)
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
if (word1.charAt(i-1) == word2.charAt(j-1))
dp[i][j] = dp[i-1][j-1];
else
dp[i][j] = 1 + Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]));
return dp[m][n];
}
2-4) Fibonacci Number (LC 509) — Recursion → Memoization → DP → O(1)
Classic example showing all 4 levels of optimization from naive recursion.
// LC 509 - Fibonacci Number
// IDEA: Iterative DP — O(N) time, O(1) space (vs O(2^N) naive recursion)
// time = O(N), space = O(1)
public int fib(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
2-5) Unique Paths (LC 62) — 2D Recursion → DP
Recursion
f(i,j) = f(i-1,j) + f(i,j-1)→ bottom-up DP with row compression.
// LC 62 - Unique Paths
// IDEA: DP with 1D rolling array — dp[j] = paths to reach column j in current row
// time = O(M*N), space = O(N)
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
dp[j] += dp[j-1];
return dp[n-1];
}
2-6) Triangle (LC 120) — Top-Down Recursion → Bottom-Up DP
dp[i][j] = min path sum from (i,j) to bottom; convert triangle recursion bottom-up.
// LC 120 - Triangle
// IDEA: Bottom-up DP in-place — start from second-to-last row, accumulate minimum path
// time = O(N^2), space = O(1) modifying input
public int minimumTotal(List<List<Integer>> triangle) {
int n = triangle.size();
int[] dp = new int[n];
for (int i = 0; i < n; i++) dp[i] = triangle.get(n-1).get(i);
for (int row = n-2; row >= 0; row--)
for (int col = 0; col <= row; col++)
dp[col] = triangle.get(row).get(col) + Math.min(dp[col], dp[col+1]);
return dp[0];
}
2-7) Longest Palindromic Subsequence (LC 516) — Interval DP
dp[i][j]= LPS length in s[i…j]; recursionf(i,j)→ bottom-up by increasing length.
// LC 516 - Longest Palindromic Subsequence
// IDEA: Interval DP — dp[i][j] = LPS in s[i..j]
// time = O(N^2), space = O(N^2)
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) dp[i][i] = 1;
for (int len = 2; len <= n; len++)
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i+1][j-1] + 2;
else dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);
}
return dp[0][n-1];
}
2-8) Coin Change (LC 322) — Recursion with Pruning → DP
Recursion
f(amount) = 1 + min(f(amount-coin))→ bottom-up unbounded knapsack DP.
// LC 322 - Coin Change
// IDEA: Bottom-up DP — dp[i] = min coins for amount i; unbounded knapsack
// time = O(amount * |coins|), space = O(amount)
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
for (int coin : coins)
if (coin <= i) dp[i] = Math.min(dp[i], dp[i - coin] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
2-9) Combination Sum IV (LC 377) — Recursion → DP (Order Matters)
dp[i] = number of ordered combinations summing to i; unlike knapsack, order of adding matters.
// LC 377 - Combination Sum IV
// IDEA: DP — dp[i] = number of ordered ways to reach sum i
// time = O(target * |nums|), space = O(target)
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++)
for (int num : nums)
if (num <= i) dp[i] += dp[i - num];
return dp[target];
}
2-10) Minimum Cost to Cut a Stick (LC 1547) — Interval DP
dp[i][j] = min cost to make all cuts between cut[i] and cut[j]; try each middle cut.
// LC 1547 - Minimum Cost to Cut a Stick
// IDEA: Interval DP — insert endpoints; dp[i][j] = min cost for cuts between i and j
// time = O(M^3), space = O(M^2) M = cuts.length
public int minCost(int n, int[] cuts) {
int m = cuts.length;
int[] c = new int[m + 2];
c[0] = 0; c[m+1] = n;
for (int i = 0; i < m; i++) c[i+1] = cuts[i];
Arrays.sort(c);
int[][] dp = new int[m+2][m+2];
for (int len = 2; len <= m+1; len++)
for (int i = 0; i + len <= m+1; i++) {
int j = i + len;
dp[i][j] = Integer.MAX_VALUE;
for (int k = i+1; k < j; k++)
dp[i][j] = Math.min(dp[i][j], c[j]-c[i] + dp[i][k] + dp[k][j]);
}
return dp[0][m+1];
}
2-11) Partition Array for Maximum Sum (LC 1043) — 1D DP
dp[i] = max sum of partitioning array up to index i; try all sub-partitions of size 1…k.
// LC 1043 - Partition Array for Maximum Sum
// IDEA: DP — dp[i] = max sum when array[0..i-1] is partitioned
// time = O(N * k), space = O(N)
public int maxSumAfterPartitioning(int[] arr, int k) {
int n = arr.length;
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
int maxVal = 0;
for (int j = 1; j <= k && i - j >= 0; j++) {
maxVal = Math.max(maxVal, arr[i-j]);
dp[i] = Math.max(dp[i], dp[i-j] + maxVal * j);
}
}
return dp[n];
}