DSA Dynamic Programming
Dynamic Programming
The query hits at n = 40 catalog SKUs and a budget of 10⁴. Brute-force subsets are correct; 2⁴⁰ is not a one-second answer. You need work proportional to a , not to the full recursion tree.
DP is not “use a table.” It requires , , and a precise before any cache or loop. Then choose or .
Algorithm journey
What inputs uniquely determine a subproblem answer?
Can an optimum be built from optima of those states?
Will the same state recur enough to pay for caching?
Memoize or tabulate
Compute each reachable state once; extract the answer.
What actually happens
Naive idea: recurse on every choice and recompute shared prefixes. Correct on tiny n. On grid paths or knapsack-style budgets, the call tree explodes while the distinct states stay polynomial (or pseudo-polynomial).
Better idea: name the , prove and , then store each answer once. Time becomes (reachable states) × (transitions per state), not the size of the naive tree.
The improved design still fails when the state misses a field (wrong cache key), when subproblems do not overlap, when the state space is exponential and you needed approximation, or when you optimize without optimal substructure.
Invariant
After a state is fully solved, its stored value equals the true answer for that exact subproblem. Every transition may read only already-correct predecessor states (or recurse into them under memoization).
How to choose DP
1. Can you define the state? If you cannot name the inputs that fix the answer, do not open a DP table yet.
2. Optimal substructure + overlap? Both required for classic DP. Overlap alone is caching; optimum alone may still need search.
3. Top-down or bottom-up? Natural recursion and sparse reachability favor memoization. Tight loops, space rolling, and no stack risk favor tabulation.
Quantitative check
Naive Fibonacci recursion is Θ(φⁿ) calls. Memoized Fib is Θ(n) states. A 100×100 grid path DP is about 10⁴ cells; unrestricted path enumeration is binomial and huge. At n = 40 subset DP without structure, 2⁴⁰ states are impossible: compress the state or change the model.
Grid Paths Visualizer (2D DP)
Counting unique paths from top-left to bottom-right.
Step 0 of -1
Core principles
1. Overlapping subproblems
The problem breaks into smaller subproblems that are solved multiple times. Solve each once and store the result.
Example: Computing Fibonacci(5) requires Fibonacci(3) multiple times
2. Optimal substructure
The optimal solution can be constructed from optimal solutions of its subproblems.
Example: Shortest path from A to C through B = shortest(A to B) + shortest(B to C) when that decomposition is valid
DP vs divide and conquer
Both break problems into subproblems. Divide and conquer solves independent subproblems (merge sort halves). DP solves overlapping subproblems and remembers answers.
Two approaches
🔝 Top-Down (Memoization)
- •Start with original problem, break down recursively
- •Store results in cache (memo) as you solve
- •Check cache before solving subproblem
- •Natural recursive thinking
- •Only solves needed subproblems
⬇️ Bottom-Up (Tabulation)
- •Start with smallest subproblems, build up
- •Fill table/array iteratively
- •No recursion overhead
- •Better space optimization possible
- •Solves all subproblems
🏆 Memoization vs. Tabulation: Which to choose?
| Feature | Top-Down (Memoization) | Bottom-Up (Tabulation) |
|---|---|---|
| Implementation | Recursive (Natural for many) | Iterative (Requires clear ordering) |
| Subproblems solved | Only those needed for the solution | All subproblems in the table |
| Overhead | Recursive calls (Call stack overhead) | No recursion (Faster execution) |
| Space Optimization | Difficult to optimize away the cache | Easier (e.g., use only last 2 rows/elements) |
| Risk | Stack Overflow for large 'N' | No recursion-stack risk; table size and numeric overflow still matter |
State design & DAG of subproblems
A DP formulation is a directed acyclic graph: each state points only to strictly smaller states in a well-founded measure (smaller index, remaining budget, bitmask subset, etc.). If you cannot define such an order, you may have a cycle, pure DP will not terminate without extra structure.
Dimension checklist: What uniquely identifies a subproblem? (index i, end j, sum s, mask of used items, last digit …) The product of domain sizes is your table size, watch for exponential blow-up before coding.
The six-line DP contract
1 · State
A complete, minimal description of one subproblem. If two calls can have different answers, their keys must differ.
2 · Meaning
Write one sentence: “dp[…] equals …”. This sentence determines dimensions, base cases, and the final lookup.
3 · Transition
Enumerate every legal first/last choice exactly once, then combine already-correct smaller states.
4 · Base cases
Smallest valid states plus explicit impossible states. Distinguish impossible from a legitimate zero or negative answer.
5 · Order
A topological order of the implicit state DAG: recursion postorder, increasing index, interval length, or popcount.
6 · Answer
Name the exact state or aggregate returned, and store parent decisions if the problem asks for a witness.
Complexity: count the state graph
time = reachable states × transitions examined per state
space = stored states + recursion depth + reconstruction metadata
Do not say “there are two loops, so O(n²)” until dimensions are named. A state (index, remainingCapacity) gives O(nW) pseudo-polynomial time: W is a numeric value, not its bit length. Sparse memoization may visit fewer states; hash and tuple-key costs still count.
Common archetypes and the hidden choice
| Archetype | State clue | Transition choice |
|---|---|---|
| Prefix / sequence | Best answer using first i items | Skip/take, or choose previous compatible index |
| Grid | Position plus limited resource/state | Which predecessor cell or movement arrives here |
| Two strings | Prefix lengths (i,j) | Match, delete, insert, replace, or skip one side |
| Knapsack | Item index + budget | Exclude/include; loop direction distinguishes 0/1 from unbounded |
| Interval | Contiguous [left,right] | Last operation or split point k |
| Tree / DAG | Node + decision inherited from parent | Combine independent child states after dependencies |
Optimization ladder
- 1. Prune the state: remove dimensions derivable from others; canonicalize symmetric states.
- 2. Skip unreachable states: top-down memoization or sparse maps help when the nominal table is mostly empty.
- 3. Compress memory: rolling rows are valid only when transitions no longer read overwritten history. Keep parent choices if reconstruction is required.
- 4. Accelerate transitions: prefix minima/sums, monotone queues, bitsets, coordinate compression, or specialized data structures.
- 5. Use advanced optimization only with proof: divide-and-conquer, Knuth, and convex-hull optimization require monotonicity/convexity conditions; they are not syntax templates.
When DP is not the answer
- Greedy suffices: if an exchange argument proves a local choice is globally safe (activity selection, Huffman merging, some scheduling).
- No overlap: divide-and-conquer like merge sort, subproblems are disjoint, so memo tables buy nothing.
- State space too large: meet-in-the-middle, branch-and-bound, or approximation algorithms may be required instead of exact DP.
🎯 Key Insight: The DP Pattern
To solve a problem with DP, follow these steps:
- Identify if the problem has overlapping subproblems and optimal substructure
- Define the state (what parameters uniquely identify a subproblem?)
- Write the recurrence relation (how to compute state from smaller states?)
- Determine base cases (smallest subproblems with known answers)
- Choose top-down (memoization) or bottom-up (tabulation)
- Optimize space if possible (often can reduce from 2D to 1D)
💻 Classic Example: Fibonacci Numbers
❌ Naive Recursion (Exponential Time)
Python
def fibonacci_naive(n):
"""
Naive recursive approach - VERY SLOW!
Time: O(2^n) - exponential
Space: O(n) - recursion stack
Problem: Recalculates same values many times
fib(5) calls fib(3) twice, fib(2) three times, etc.
"""
if n < 0:
raise ValueError("n must be non-negative")
if n <= 1:
return n
return fibonacci_naive(n - 1) + fibonacci_naive(n - 2)
# For n=40, this takes several seconds!
print(fibonacci_naive(10)) # 55Java
import java.math.BigInteger;
public class FibonacciNaive {
/**
* Naive recursive approach - VERY SLOW!
* Time: O(2^n) - exponential
* Space: O(n) - recursion stack
*
* Problem: Recalculates same values many times
* fib(5) calls fib(3) twice, fib(2) three times, etc.
*/
public static BigInteger fibonacciNaive(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
if (n <= 1) {
return BigInteger.valueOf(n);
}
return fibonacciNaive(n - 1).add(fibonacciNaive(n - 2));
}
public static void main(String[] args) {
// For n=40, this takes several seconds!
System.out.println(fibonacciNaive(10)); // 55
}
}✅ Top-Down DP (Memoization)
Python
def fibonacci_memo(n, memo=None):
"""
Top-down DP with memoization
Time: O(n) - each subproblem solved once
Space: O(n) - memo dictionary + recursion stack
Stores results to avoid recalculation
"""
if n < 0:
raise ValueError("n must be non-negative")
if memo is None:
memo = {}
# Check if already computed
if n in memo:
return memo[n]
# Base cases
if n <= 1:
return n
# Compute and store
memo[n] = fibonacci_memo(n - 1, memo) + fibonacci_memo(n - 2, memo)
return memo[n]
# For n=40, this is instant!
print(fibonacci_memo(100)) # 354224848179261915075Java
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
public class FibonacciMemo {
/**
* Top-down DP with memoization
* Time: O(n) - each subproblem solved once
* Space: O(n) - memo map + recursion stack
*
* Stores results to avoid recalculation
*/
public static BigInteger fibonacciMemo(int n, Map<Integer, BigInteger> memo) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
// Check if already computed
if (memo.containsKey(n)) {
return memo.get(n);
}
// Base cases
if (n <= 1) {
return BigInteger.valueOf(n);
}
// Compute and store
BigInteger result = fibonacciMemo(n - 1, memo).add(fibonacciMemo(n - 2, memo));
memo.put(n, result);
return result;
}
public static void main(String[] args) {
Map<Integer, BigInteger> memo = new HashMap<>();
BigInteger expected = new BigInteger("354224848179261915075");
if (!fibonacciMemo(100, memo).equals(expected)) throw new AssertionError();
}
}✅ Bottom-Up DP (Tabulation)
Python
def fibonacci_tabulation(n):
"""
Bottom-up DP with tabulation
Time: O(n) - single loop
Space: O(n) - dp array
Builds solution iteratively from base cases
"""
if n < 0:
raise ValueError("n must be non-negative")
if n <= 1:
return n
# Create table
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
# Fill table bottom-up
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
print(fibonacci_tabulation(100)) # 354224848179261915075Java
import java.math.BigInteger;
public class FibonacciTabulation {
/**
* Bottom-up DP with tabulation
* Time: O(n) - single loop
* Space: O(n) - dp array
*
* Builds solution iteratively from base cases
*/
public static BigInteger fibonacciTabulation(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
if (n <= 1) {
return BigInteger.valueOf(n);
}
// Create table
BigInteger[] dp = new BigInteger[n + 1];
dp[0] = BigInteger.ZERO;
dp[1] = BigInteger.ONE;
// Fill table bottom-up
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1].add(dp[i - 2]);
}
return dp[n];
}
public static void main(String[] args) {
BigInteger expected = new BigInteger("354224848179261915075");
if (!fibonacciTabulation(100).equals(expected)) throw new AssertionError();
}
}🚀 Space-Optimized DP
Python
def fibonacci_optimized(n):
"""
Space-optimized bottom-up DP
Time: O(n)
Space: O(1) - only two variables!
We only need last two values, not entire array
"""
if n < 0:
raise ValueError("n must be non-negative")
if n <= 1:
return n
prev2 = 0 # fib(0)
prev1 = 1 # fib(1)
for i in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1
print(fibonacci_optimized(100)) # 354224848179261915075Java
import java.math.BigInteger;
public class FibonacciOptimized {
/**
* Space-optimized bottom-up DP
* Time: O(n)
* Space: O(1) - only two variables!
*
* We only need last two values, not entire array
*/
public static BigInteger fibonacciOptimized(int n) {
if (n < 0) throw new IllegalArgumentException("n must be non-negative");
if (n <= 1) {
return BigInteger.valueOf(n);
}
BigInteger prev2 = BigInteger.ZERO; // fib(0)
BigInteger prev1 = BigInteger.ONE; // fib(1)
for (int i = 2; i <= n; i++) {
BigInteger current = prev1.add(prev2);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
public static void main(String[] args) {
BigInteger expected = new BigInteger("354224848179261915075");
if (!fibonacciOptimized(100).equals(expected)) throw new AssertionError();
}
}📊 Performance Comparison
| Approach | Time | Space | n=40 Runtime |
|---|---|---|---|
| Naive Recursion | O(2ⁿ) | O(n) | ~30 seconds |
| Memoization | O(n) | O(n) | ~0.001 seconds |
| Tabulation | O(n) | O(n) | ~0.001 seconds |
| Optimized | O(n) | O(1) | ~0.001 seconds |
💡 Pro Tip: When to Use DP
Ask yourself these questions to identify DP problems:
- • Can the problem be broken into overlapping subproblems?
- • Does it have optimal substructure (optimal solution from optimal subproblems)?
- • Are you finding maximum/minimum or counting ways?
- • Does naive recursion have repeated calculations?
Common DP keywords: "maximum", "minimum", "longest", "shortest", "count ways", "optimize"
🧩 Advanced DP Patterns
1. Longest Common Subsequence (LCS)String DP
Finding the longest subsequence present in both strings in the same relative order. Unlike substrings, subsequences are not required to occupy consecutive positions.
Recurrence Relation:
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])
- ✅ Base Case: dp[i][0] = 0, dp[0][j] = 0
- 🚀 Time: O(N * M)
- 💾 Space: O(N * M) or O(min(N, M))
Python
def lcs(s1, s2):
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
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]
assert lcs("AGGTAB", "GXTXAYB") == 4
assert lcs("🙂A", "🙂B") == 1Java
public class LcsDP {
static int lcs(String first, String second) {
int[] firstPoints = first.codePoints().toArray();
int[] secondPoints = second.codePoints().toArray();
int[][] dp = new int[firstPoints.length + 1][secondPoints.length + 1];
for (int i = 1; i <= firstPoints.length; i++) {
for (int j = 1; j <= secondPoints.length; j++) {
if (firstPoints[i - 1] == secondPoints[j - 1]) {
dp[i][j] = 1 + dp[i - 1][j - 1];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[firstPoints.length][secondPoints.length];
}
public static void main(String[] args) {
if (lcs("AGGTAB", "GXTXAYB") != 4 || lcs("🙂A", "🙂B") != 1) {
throw new AssertionError();
}
}
}2. Longest Increasing Subsequence (LIS)Array DP
Finding the length of the longest subsequence in an array such that all elements of the subsequence are sorted in increasing order.
State Definition:
dp[i] = length of LIS ending exactly at index i.
Recurrence:
dp[i] = 1 + max(dp[j]) for all j < i such that arr[j] < arr[i]
- 🚀 Time: O(N²) (Basic), O(N log N) (Binary Search)
- 💾 Space: O(N)
Python
def length_of_lis(nums):
if not nums: return 0
n = len(nums)
dp = [1] * n
for i in range(1, n):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
assert length_of_lis([10, 9, 2, 5, 3, 7, 101, 18]) == 4Java
import java.util.Arrays;
public class LisDP {
static int lengthOfLis(int[] values) {
if (values.length == 0) return 0;
int[] dp = new int[values.length];
Arrays.fill(dp, 1);
int answer = 1;
for (int i = 1; i < values.length; i++) {
for (int j = 0; j < i; j++) {
if (values[j] < values[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
answer = Math.max(answer, dp[i]);
}
return answer;
}
public static void main(String[] args) {
int[] input = {10, 9, 2, 5, 3, 7, 101, 18};
if (lengthOfLis(input) != 4) throw new AssertionError();
}
}✅ Pros & ❌ Cons
✅ Advantages
- ✓Evaluates each reachable state once instead of repeating the same subproblem
- ✓Computes the exact recurrence when the state, transitions, base cases, and order are correct
- ✓Avoids redundant calculations
- ✓Systematic approach to optimization problems
- ✓Can be space-optimized
❌ Disadvantages
- ✗Can be difficult to identify DP problems
- ✗Requires extra space for memoization/table
- ✗Finding recurrence relation can be tricky
- ✗Not all optimization problems have DP solution
- ✗Can have high space complexity
💼 Real World Applications
🗺️ GPS & Route Planning
GPS systems use DP (Bellman-Ford, Floyd-Warshall) to find shortest paths between locations, storing intermediate results to quickly recalculate when routes change.
Example: Google Maps, Waze navigation
📱 Autocomplete & Spell Check
Edit distance (Levenshtein distance) uses DP to find the minimum number of edits to transform one word to another, powering spell checkers and autocorrect.
Example: Smartphone keyboards, search suggestions
💰 Finance & Trading
Portfolio optimization, option pricing (binomial model), and algorithmic trading use DP to maximize returns while managing risk.
Example: Stock trading algorithms, risk management
🧬 Bioinformatics
DNA sequence alignment uses DP (Needleman-Wunsch algorithm) to find similarities between genetic sequences, crucial for understanding evolution and disease.
Example: Genome sequencing, protein folding
Exercises
- For grid paths from (0,0) to (n,n) moving only right/down, write the and prove with a tiny counterexample recursion tree (n = 3).
- n = 10⁵ stairs, climb 1 or 2. Compare naive recursion, memoization, and O(1)-space tabulation. Where does the call stack still fail?
- Give a problem with optimal substructure but no useful overlap (or the reverse). Would you still call it DP?
Primary sources
- Bellman, R. (1957): Dynamic Programming (principle of optimality).
- Cormen et al., Introduction to Algorithms (CLRS): dynamic programming chapter (optimal substructure, overlapping subproblems).
- Kleinberg & Tardos, Algorithm Design: DP design patterns and weighted interval scheduling.
Takeaway
State first, then optimal substructure and overlapping subproblems. Memoization and tabulation are delivery mechanisms for the same recurrence.