Memoization(top-down):Recursive+ cache results of subproblems.Tabulation(bottom-up):Iterative+ fill a table from base cases up.Both avoid recomputing overlapping subproblems.
Example
// JavaScript - Fibonacci both ways// Top-down (memoization)functionfibMemo(n, memo ={}){if(n <=1)return n;if(n in memo)return memo[n];
memo[n]=fibMemo(n -1, memo)+fibMemo(n -2, memo);return memo[n];}// Bottom-up (tabulation)functionfibTab(n){if(n <=1)return n;const dp =[0,1];for(let i =2; i <= n; i++){
dp[i]= dp[i -1]+ dp[i -2];}return dp[n];}
# Python
# Top-down
def fib_memo(n, memo={}):if n <=1:return n
if n not in memo:
memo[n]=fib_memo(n -1, memo)+fib_memo(n -2, memo)return memo[n]
# Bottom-up
def fib_tab(n):if n <=1:return n
dp =[0,1]for i inrange(2, n +1):
dp.append(dp[-1]+ dp[-2])return dp[n]
Output
fib_memo(10) → 55
fib_tab(10) → 55
Note Both are O(n) time, O(n) space. Memoization is easier to write (natural recursion) but has call stack overhead. Tabulation avoids stack overflow and can be space-optimized (keep only last 2 values → O(1) space). Start with memoization in interviews, then optimize to tabulation if asked.
Note Time O(n), Space O(1) with the two-variable optimization. This is literally Fibonacci shifted by one index. Classic DP warm-up - if you see this, solve it fast and mention the Fibonacci connection. Generalize to k steps: dp[i] = sum of dp[i-1]...dp[i-k].
climbing stairsstaircase problemfibonacci variantways to reach top
Coin Change - Minimum Coins
Syntax
dp[amount]= minimum coins needed to make that amount.
dp[0]=0For each amount,try each coin: dp[amount]=min(dp[amount], dp[amount - coin]+1)
Example
// JavaScriptfunctioncoinChange(coins, amount){const dp =newArray(amount +1).fill(Infinity);
dp[0]=0;for(let a =1; a <= amount; a++){for(const coin of coins){if(coin <= a && dp[a - coin]!==Infinity){
dp[a]=Math.min(dp[a], dp[a - coin]+1);}}}return dp[amount]===Infinity?-1: dp[amount];}
# Python
def coin_change(coins, amount):
dp =[float('inf')]*(amount +1)
dp[0]=0for a inrange(1, amount +1):for coin in coins:if coin <= a and dp[a - coin]!=float('inf'):
dp[a]=min(dp[a], dp[a - coin]+1)return dp[amount]if dp[amount]!=float('inf')else-1
Note Time O(amount * numCoins), Space O(amount). Greedy (always pick largest coin) does NOT work for all coin sets - DP is needed. Variant: number of ways to make change (count combinations instead of minimize). Return -1 if amount cannot be formed.
2DDP: dp[i][j]=LCS length of first i chars of s1 and first j chars of s2.If s1[i-1]== s2[j-1]: dp[i][j]= dp[i-1][j-1]+1Else: dp[i][j]=max(dp[i-1][j], dp[i][j-1])
Example
// JavaScriptfunctionlongestCommonSubsequence(s1, s2){const m = s1.length, n = s2.length;const dp =Array.from({ length: m +1},()=>newArray(n +1).fill(0));for(let i =1; i <= m; i++){for(let j =1; j <= n; j++){if(s1[i -1]=== s2[j -1]){
dp[i][j]= dp[i -1][j -1]+1;}else{
dp[i][j]=Math.max(dp[i -1][j], dp[i][j -1]);}}}return dp[m][n];}
# Python
def longest_common_subsequence(s1, s2):
m, n =len(s1),len(s2)
dp =[[0]*(n +1)for _ inrange(m +1)]for i inrange(1, m +1):for j inrange(1, n +1):if s1[i -1]== s2[j -1]:
dp[i][j]= dp[i -1][j -1]+1else:
dp[i][j]=max(dp[i -1][j], dp[i][j -1])return dp[m][n]
Note Time O(m*n), Space O(m*n), reducible to O(min(m,n)) with rolling array. To reconstruct the actual subsequence, backtrack through the DP table. LCS is the foundation for diff algorithms and edit distance. Subsequence ≠ substring (subsequence elements need not be contiguous).
longest common subsequenceLCS2D DPsubsequence matching
0/1 Knapsack
Syntax
Given items with weight and value, maximize value within capacity.Each item can be taken or left(0 or 1 times).
dp[i][w]= max value using first i items with capacity w.
Example
// JavaScriptfunctionknapsack(weights, values, capacity){const n = weights.length;const dp =Array.from({ length: n +1},()=>newArray(capacity +1).fill(0));for(let i =1; i <= n; i++){for(let w =0; w <= capacity; w++){
dp[i][w]= dp[i -1][w];// skip itemif(weights[i -1]<= w){
dp[i][w]=Math.max(
dp[i][w],
dp[i -1][w - weights[i -1]]+ values[i -1]);}}}return dp[n][capacity];}
# Python
def knapsack(weights, values, capacity):
n =len(weights)
dp =[[0]*(capacity +1)for _ inrange(n +1)]for i inrange(1, n +1):for w inrange(capacity +1):
dp[i][w]= dp[i -1][w]if weights[i -1]<= w:
dp[i][w]=max(dp[i][w],
dp[i -1][w - weights[i -1]]+ values[i -1])return dp[n][capacity]
Output
knapsack([2,3,4,5], [3,4,5,6], 5) → 7 (items with weight 2+3, value 3+4)
Note Time O(n * capacity), Space O(n * capacity), reducible to O(capacity) with 1D DP (iterate weights backwards). The 0/1 constraint means we look at dp[i-1] (previous row). For unbounded knapsack (items reusable), look at dp[i] (current row). Recognize the pattern: subset selection with a constraint.
dp[i]= length ofLIS ending at index i.For each i, check all j < i:if arr[j]< arr[i], dp[i]=max(dp[i], dp[j]+1).Optimal: patience sorting with binary search → O(n log n).
Example
// JavaScript - O(n^2) DPfunctionlisDP(arr){const dp =newArray(arr.length).fill(1);for(let i =1; i < arr.length; i++){for(let j =0; j < i; j++){if(arr[j]< arr[i]) dp[i]=Math.max(dp[i], dp[j]+1);}}returnMath.max(...dp);}// O(n log n) with binary searchfunctionlisBinarySearch(arr){const tails =[];for(const num of arr){let lo =0, hi = tails.length;while(lo < hi){const mid = lo +Math.floor((hi - lo)/2);if(tails[mid]< num) lo = mid +1;else hi = mid;}
tails[lo]= num;}return tails.length;}
# Python-O(n log n)import bisect
def lis(arr):
tails =[]for num in arr:
pos = bisect.bisect_left(tails, num)if pos ==len(tails):
tails.append(num)else:
tails[pos]= num
returnlen(tails)
Output
lis([10,9,2,5,3,7,101,18]) → 4 (subsequence: 2,3,7,101 or 2,5,7,101)
Note O(n^2) DP is straightforward. O(n log n) uses a 'tails' array: tails[i] = smallest tail element for all increasing subsequences of length i+1. The tails array is NOT the actual LIS. To reconstruct, keep a parent pointer array. This is a very popular hard DP problem.
Minimumoperations(insert,delete, replace) to transform s1 into s2.
dp[i][j]= edit distance for s1[0..i-1] and s2[0..j-1].If chars match: dp[i][j]= dp[i-1][j-1]Else:1+min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
Example
// JavaScriptfunctioneditDistance(s1, s2){const m = s1.length, n = s2.length;const dp =Array.from({ length: m +1},(_, i)=>Array.from({ length: n +1},(_, j)=>(i ===0? j : j ===0? i :0)));for(let i =1; i <= m; i++){for(let j =1; j <= n; j++){if(s1[i -1]=== s2[j -1]){
dp[i][j]= dp[i -1][j -1];}else{
dp[i][j]=1+Math.min(
dp[i -1][j],// delete
dp[i][j -1],// insert
dp[i -1][j -1]// replace);}}}return dp[m][n];}
# Python
def edit_distance(s1, s2):
m, n =len(s1),len(s2)
dp =[[0]*(n +1)for _ inrange(m +1)]for i inrange(m +1):
dp[i][0]= i
for j inrange(n +1):
dp[0][j]= j
for i inrange(1, m +1):for j inrange(1, n +1):if s1[i -1]== s2[j -1]:
dp[i][j]= dp[i -1][j -1]else:
dp[i][j]=1+min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])return dp[m][n]
Note Time O(m*n), Space O(m*n), reducible to O(min(m,n)). Base cases: transforming empty string to s requires len(s) operations. Used in spell checkers, DNA analysis, and fuzzy matching. To reconstruct the operations, backtrack through the DP table.
Cannot rob two adjacent houses.Maximize total.
dp[i]=max(dp[i-1], dp[i-2]+ nums[i])At each house: skip it(take prev best) or rob it(prev-prev best + current).
Example
// JavaScriptfunctionrob(nums){if(nums.length===0)return0;if(nums.length===1)return nums[0];let prev2 =0, prev1 =0;for(const num of nums){const current =Math.max(prev1, prev2 + num);
prev2 = prev1;
prev1 = current;}return prev1;}
# Python
def rob(nums):
prev2, prev1 =0,0for num in nums:
prev2, prev1 = prev1,max(prev1, prev2 + num)return prev1
Note Time O(n), Space O(1). The circular variant (houses in a circle): run the algorithm twice - once excluding the first house, once excluding the last - and take the max. This pattern of 'take or skip' with constraints appears in many DP problems.
house robbermaximum non-adjacenttake or skipDP optimizationcircular variant