Optimal substructure + overlapping subproblems solved via top-down memoization or bottom-up tabulation. Master Fibonacci, 0/1 Knapsack, LCS, LIS, Coin Change, Edit Distance and more.
Algorithms
Dynamic Programming Memoization
Dynamic Programming (DP) transforms exponential recursive solutions into polynomial-time algorithms by storing and reusing the results of overlapping subproblems. It applies when a problem has optimal substructure (an optimal solution is built from optimal sub-solutions) and overlapping subproblems (the same subproblem recurs many times). DP is the single most important algorithm paradigm in coding interviews.
01
Section One ยท Foundation
What is Dynamic Programming?
Dynamic programming breaks a problem into smaller overlapping subproblems, solves each once, stores the result in a table (memoisation or DP array), and reuses stored results to avoid redundant computation. The classic naive recursive Fibonacci has O(2โฟ) time because it recomputes f(k) for every path; DP reduces this to O(n) by computing each f(k) exactly once. Two implementation styles exist: top-down memoization (recursive with a cache) and bottom-up tabulation (iterative, filling a table from base cases up).
Analogy: Imagine computing the total cost of all routes through a city. Instead of recalculating the distance from node B to the destination every time you arrive at B by a different route, you write it down the first time. The next time you reach B, you look up the stored answer in O(1). This is exactly what the DP table does โ a lookup table of pre-solved subproblem answers.
Prerequisites for DP Applicability
Optimal Substructure
โถ
The optimal solution to the full problem can be constructed from optimal solutions to sub-problems (e.g. shortest path, minimum cost).
Overlapping Subproblems
โถ
The same sub-problems recur โ unlike divide-and-conquer (Merge Sort) which creates independent non-overlapping sub-problems.
Define the DP State
โถ
dp[i] or dp[i][j] must fully describe the sub-problem. The state definition is the hardest and most important part of DP.
Recurrence Relation
โถ
Express dp[i] as a function of smaller states (e.g. dp[i] = dp[i-1] + dp[i-2] for Fibonacci). Write this before coding.
Top-Down Memoization
Recursive + cache (HashMap or array)
Only solves needed subproblems
Easier to write from the recursive formulation
Stack overflow risk on deep recursion
Bottom-Up Tabulation
Iterative + DP table (array)
Solves all subproblems in topological order
Better cache performance, no recursion overhead
Can often optimise space (rolling array)
02
Section Two ยท Core Patterns
DP Patterns & Problem Families
1D DP โ Linear Sequence (Fibonacci, Climbing Stairs, House Robber)
State depends on a constant number of previous states. dp[i] = f(dp[i-1], dp[i-2], โฆ). Space can usually be compressed to O(1).
Step
Action (Fibonacci)
State After
1
Define dp[i] = i-th Fibonacci number; base cases dp[0]=0, dp[1]=1
dp[0..1] initialized
2
Fill dp[i] = dp[i-1] + dp[i-2] for i from 2 to n
dp[0..n] fully filled
3
Return dp[n]; optimise to O(1) space using two variables prev, curr
Answer in O(n) time, O(1) space
Pattern: Whenever you see "number of ways to reach step n" or "max profit with no two adjacent elements selected", it's 1D DP. Recognise the recurrence first, then implement.
State depends on two variables (indices into two strings/sequences, or item+capacity). dp[i][j] = optimal answer for first i elements of A and first j elements of B.
Step
Action (LCS โ Longest Common Subsequence)
State After
1
dp[i][j] = LCS length of A[0..i-1] and B[0..j-1]; base: dp[0][j]=dp[i][0]=0
First row/column zeroed
2
If A[i-1]==B[j-1]: dp[i][j] = dp[i-1][j-1]+1; else dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Table filled row by row
3
Answer is dp[m][n]; trace back for the actual subsequence if needed
O(mn) time, O(mn) space (or O(n) with rolling row)
Pattern: Two-pointer strings โ 2D DP. Edit Distance, LCS, and Longest Palindromic Subsequence all use this exact structure. For 0/1 Knapsack, dimensions are items ร capacity.
dp[i][j] = optimal answer for the sub-array/interval [i, j]. Fill by increasing interval length, trying every split point k.
Step
Action (Matrix Chain Multiplication)
State After
1
dp[i][j] = min multiplications for chain from matrix i to j; base: dp[i][i]=0
Diagonal zeroed
2
For each length L=2..n, for each i, j=i+L-1: try all split points k โ [i,j-1]
dp[i][j] = min(dp[i][k] + dp[k+1][j] + cost)
3
Answer at dp[0][n-1]
O(nยณ) time, O(nยฒ) space
Pattern: Interval DP is identified by "minimum/maximum cost to reduce an interval" โ burst balloons, stone merging, palindrome partitioning. The template is always: for length, for start, for split point.
Space Optimisation โ O(nยฒ) โ O(n) โ O(1)
Many DP tables only look back one or two rows; the previous rows can be discarded (rolling array). 1D DP often reduces to two variables.
Step
Action
State After
1
Identify which previous states dp[i][j] depends on (row i-1 only? Column j-1?)
Dependency graph clear
2
Replace dp[i] with a 1D array; for 0/1 Knapsack iterate capacity backwards to prevent overwrite
Space: O(capacity) instead of O(n*capacity)
3
For Fibonacci/Climbing Stairs: two variables prev, curr suffice โ O(1) space
Maximum compression achieved
Tip: For 0/1 Knapsack space optimisation, iterate the capacity dimension backwards (from high to low) to ensure each item is counted at most once. Iterating forwards would allow an item to be picked multiple times (Unbounded Knapsack).
Common Mistakes:
Wrong state definition: The state must fully capture the sub-problem. Missing a dimension (e.g. index, remaining capacity) leads to incorrect recurrences.
Off-by-one in base cases: dp array size should be n+1 for 1-indexed problems. Initialising dp[0] incorrectly breaks the entire table.
Greedy when DP is needed: If the choice at step i affects what's available at step i+k, greedy fails. Use DP when future choices depend on past decisions.
03
Section Three ยท Visuals
Diagrams & Walkthroughs
Fibonacci: Naive Recursion vs Memoization
Coin Change โ Bottom-Up DP Table
LCS (Longest Common Subsequence) โ 2D DP Table
04
Section Four ยท Code
Implementations
Quick-reference โ Fibonacci top-down vs bottom-up. Click the button for full implementations of Coin Change, LCS, Knapsack, Edit Distance, and LIS.
// Fibonacci โ Top-Down (Memoization) vs Bottom-Up// Top-Down O(n) time, O(n) spaceMap<Integer, Long> memo = new HashMap<>();
longfib(int n) {
if (n <= 1) return n;
if (memo.containsKey(n)) return memo.get(n);
long res = fib(n-1) + fib(n-2);
memo.put(n, res);
return res;
}
// Bottom-Up O(n) time, O(1) spacelongfibDP(int n) {
if (n <= 1) return n;
long prev = 0, curr = 1;
for (int i = 2; i <= n; i++) { long t = prev + curr; prev = curr; curr = t; }
return curr;
}
import java.util.*;
public class DynamicProgramming {
// โโ Fibonacci (bottom-up O(1) space) โโโโโโโโ
public static long fib(int n) {
if (n <= 1) return n;
long a = 0, b = 1;
for (int i = 2; i <= n; i++) { long t = a + b; a = b; b = t; }
return b;
}
// โโ Coin Change (min coins) โโโโโโโโโโโโโโโโโโ
public static int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int a = 1; a <= amount; a++)
for (int c : coins)
if (c <= a) dp[a] = Math.min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
// โโ 0/1 Knapsack โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
public static int knapsack(int[] weights, int[] values, int W) {
int n = weights.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++)
for (int w = W; w >= weights[i]; w--)
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
return dp[W];
}
// โโ Longest Common Subsequence โโโโโโโโโโโโโโโ
public static int lcs(String a, String b) {
int m = a.length(), n = b.length();
int[] dp = new int[n + 1]; // space-optimised: 1 row
for (int i = 1; i <= m; i++) {
int prev = 0;
for (int j = 1; j <= n; j++) {
int cur = dp[j];
dp[j] = a.charAt(i-1) == b.charAt(j-1)
? prev + 1
: Math.max(dp[j], dp[j-1]);
prev = cur;
}
}
return dp[n];
}
// โโ Edit Distance (Levenshtein) โโโโโโโโโโโโโโ
public static int editDistance(String a, String b) {
int m = a.length(), n = b.length();
int[] dp = new int[n + 1];
for (int j = 0; j <= n; j++) dp[j] = j;
for (int i = 1; i <= m; i++) {
int prev = dp[0];
dp[0] = i;
for (int j = 1; j <= n; j++) {
int cur = dp[j];
dp[j] = a.charAt(i-1) == b.charAt(j-1)
? prev
: 1 + Math.min(prev, Math.min(dp[j], dp[j-1]));
prev = cur;
}
}
return dp[n];
}
// โโ Longest Increasing Subsequence (O(n log n)) โ
public static int lis(int[] nums) {
List<Integer> tails = new ArrayList<>();
for (int x : nums) {
int lo = 0, hi = tails.size();
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (tails.get(mid) < x) lo = mid + 1; else hi = mid;
}
if (lo == tails.size()) tails.add(x);
else tails.set(lo, x);
}
return tails.size();
}
}
import bisect
# Fibonacci (bottom-up O(1) space)
def fib(n):
a, b = 0, 1
for _ in range(n): a, b = b, a + b
return a
# Coin Change (min coins)
def coin_change(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for a in range(1, amount + 1):
for c in coins:
if c <= a: dp[a] = min(dp[a], dp[a - c] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# 0/1 Knapsack
def knapsack(weights, values, W):
dp = [0] * (W + 1)
for w, v in zip(weights, values):
for cap in range(W, w - 1, -1): # iterate backwards!
dp[cap] = max(dp[cap], dp[cap - w] + v)
return dp[W]
# Longest Common Subsequence
def lcs(a, b):
m, n = len(a), len(b)
dp = [0] * (n + 1)
for i in range(1, m + 1):
prev = 0
for j in range(1, n + 1):
cur = dp[j]
dp[j] = prev + 1 if a[i-1] == b[j-1] else max(dp[j], dp[j-1])
prev = cur
return dp[n]
# Edit Distance
def edit_distance(a, b):
m, n = len(a), len(b)
dp = list(range(n + 1))
for i in range(1, m + 1):
prev = dp[0]; dp[0] = i
for j in range(1, n + 1):
cur = dp[j]
dp[j] = prev if a[i-1] == b[j-1] else 1 + min(prev, dp[j], dp[j-1])
prev = cur
return dp[n]
# LIS โ O(n log n) patience sorting
def lis(nums):
tails = []
for x in nums:
i = bisect.bisect_left(tails, x)
if i == len(tails): tails.append(x)
else: tails[i] = x
return len(tails)
using System;
public class DynamicProgramming {
public static long Fib(int n) {
long a = 0, b = 1;
for (int i = 0; i < n; i++) (a, b) = (b, a + b);
return a;
}
public static int CoinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Array.Fill(dp, amount + 1); dp[0] = 0;
for (int a = 1; a <= amount; a++)
foreach (var c in coins)
if (c <= a) dp[a] = Math.Min(dp[a], dp[a - c] + 1);
return dp[amount] > amount ? -1 : dp[amount];
}
public static int Knapsack(int[] w, int[] v, int W) {
int[] dp = new int[W + 1];
for (int i = 0; i < w.Length; i++)
for (int cap = W; cap >= w[i]; cap--)
dp[cap] = Math.Max(dp[cap], dp[cap - w[i]] + v[i]);
return dp[W];
}
public static int LCS(string a, string b) {
int m = a.Length, n = b.Length;
int[] dp = new int[n + 1];
for (int i = 1; i <= m; i++) {
int prev = 0;
for (int j = 1; j <= n; j++) {
int cur = dp[j];
dp[j] = a[i-1] == b[j-1] ? prev + 1 : Math.Max(dp[j], dp[j-1]);
prev = cur;
}
}
return dp[n];
}
public static int EditDistance(string a, string b) {
int m = a.Length, n = b.Length;
int[] dp = new int[n + 1];
for (int j = 0; j <= n; j++) dp[j] = j;
for (int i = 1; i <= m; i++) {
int prev = dp[0]; dp[0] = i;
for (int j = 1; j <= n; j++) {
int cur = dp[j];
dp[j] = a[i-1] == b[j-1] ? prev : 1 + Math.Min(prev, Math.Min(dp[j], dp[j-1]));
prev = cur;
}
}
return dp[n];
}
}
05
Section Five ยท Complexity
Time & Space Complexity
Problem
Time
Space (full)
Space (optimised)
Pattern
Fibonacci / Climbing Stairs
O(n)
O(n)
O(1) โ two variables
1D linear
House Robber / Max Subarray
O(n)
O(n)
O(1)
1D linear
Coin Change (min coins)
O(n ร amount)
O(amount)
O(amount)
Unbounded Knapsack
0/1 Knapsack
O(n ร W)
O(n ร W)
O(W) โ 1D reverse
0/1 Subset
Longest Common Subsequence
O(mn)
O(mn)
O(min(m,n))
2D sequence
Edit Distance
O(mn)
O(mn)
O(n)
2D sequence
LIS (O(nยฒ) DP)
O(nยฒ)
O(n)
โ
1D dependent
LIS (patience sort)
O(n log n)
O(n)
โ
BS + tails array
Burst Balloons / Matrix Chain
O(nยณ)
O(nยฒ)
โ
Interval DP
Time = States ร Transition cost. For 1D DP: n states ร O(1) transition = O(n). For 2D: mn states ร O(1) = O(mn). For Interval DP: O(nยฒ) intervals ร O(n) split points = O(nยณ). Space can always be compressed if the recurrence only looks back a fixed number of rows.
Space Complexity Note
The key insight for space optimisation: identify which "previous" states dp[i][j] depends on. If only the previous row (dp[i-1][*]) is needed, replace the 2D array with a rolling 1D array. For 0/1 Knapsack, iterate the capacity dimension backwards so updates don't overwrite values still needed in the same pass.
06
Section Six ยท Decision Guide
When to Use Dynamic Programming
Use DP When
Problem has optimal substructure (optimal solution โ optimal sub-solutions)
Problem has overlapping subproblems (same sub-problem recurs)
Problem asks min/max/count/exists for a combinatorial structure
Brute force is exponential but the number of distinct states is polynomial
Avoid DP / Use Alternatives When
Subproblems are independent โ use Divide & Conquer (Merge Sort)
A locally greedy choice is always globally optimal โ use Greedy (activity selection)
Problem requires tracking the full path/configuration, not just the value โ Backtracking
Input is a graph without DAG structure โ graph algorithms (BFS/DFS/Dijkstra)
DP Pattern โ Problem Mapping
Pattern
State
Signature Problems
1D Sequential
dp[i]
Climbing Stairs, House Robber, Max Subarray (Kadane)
Unbounded Knapsack
dp[amount]
Coin Change, Perfect Squares, Combination Sum IV
0/1 Knapsack
dp[i][w]
Target Sum, Partition Equal Subset, Last Stone Weight II
Two-Sequence 2D
dp[i][j]
LCS, Edit Distance, Distinct Subsequences, Regex Match
Interval DP
dp[i][j]
Burst Balloons, Palindrome Partition, Stone Merge
State Machine
dp[i][state]
Best Time to Buy/Sell Stock (k transactions, cooldown)
DP on Trees
dp[node][โฆ]
House Robber III, Diameter of Tree, Max Path Sum
โ DP Thinking Process: (1) Define state clearly. (2) Write the recurrence. (3) Identify base cases. (4) Determine fill order (bottom-up). (5) Optimise space if needed. Never skip step 1.
07
Section Seven ยท Interview Practice
LeetCode Problems
DP problems test your ability to identify the optimal substructure, define a clean DP state, and write a recurrence. Group problems by pattern to build intuition โ not by difficulty.
Medium#322 โ Coin Change โ Unbounded knapsack; BFS also works for shortest path to amount.
Medium#300 โ Longest Increasing Subsequence โ O(nยฒ) DP or O(n log n) patience sorting with binary search.
Medium#1143 โ Longest Common Subsequence โ Classic 2D DP; foundation for Edit Distance and Diff tools.
Medium#416 โ Partition Equal Subset Sum โ 0/1 Knapsack: can we partition into two equal-sum subsets?
Medium#152 โ Maximum Product Subarray โ Track both max and min dp values (negatives flip sign).
Hard#72 โ Edit Distance โ 2D DP; three transitions: insert, delete, replace. Tracing back gives the actual edit sequence.
Hard#312 โ Burst Balloons โ Interval DP; think "last balloon to burst" instead of first, for clean subproblem boundaries.
Interview Pattern: If you see "minimum cost", "maximum value", "number of ways", or "can it be done?" on a sequence or grid โ DP is almost certainly the intended approach. Define the state first on paper, write the recurrence, then code. Rushing to code before defining the state is the #1 source of DP bugs in interviews.