Introduction to Dynamic Programming
1. Introduction: The Core Philosophy
Imagine you have to calculate . You count them up and say "8".
Now, imagine I add one more + 1 to the end of that sequence and ask for the total. Do you recount from the beginning? No, you just say "9". Why? Because you remembered the previous result and just added 1 to it.
This is the exact philosophy of Dynamic Programming (DP). DP is an algorithmic paradigm that solves a complex problem by breaking it down into smaller subproblems, solving each subproblem exactly once, and storing their solutions.
Many students are terrified of DP. It has a reputation as the hardest topic in coding interviews. But in reality, DP is not a specific algorithm (like Merge Sort or Dijkstra's); it is simply Backtracking + Caching. If you can write a recursive brute-force solution, you are 90% of the way to a DP solution!
2. Core Concept: The Two Requirements of DP
Before you can apply DP to a problem, the problem MUST exhibit two mathematical properties:
- Overlapping Subproblems: The problem can be broken down into smaller problems, and the exact same smaller problems are evaluated multiple times. (e.g., in Fibonacci,
fib(5)needsfib(4)andfib(3). Butfib(4)ALSO needsfib(3). We are calculatingfib(3)twice!). - Optimal Substructure: The optimal solution to the overall problem can be constructed directly from the optimal solutions of its subproblems. (e.g., the shortest path from City A to City C going through City B is simply the shortest path from A to B + the shortest path from B to C).
If a problem has these two traits, you can solve it using DP.
3. The Two Approaches: Top-Down vs Bottom-Up
Top-Down (Memoization)
- How it works: Write a standard recursive function. At the top of the function, check if the answer is already in a Cache (an array or HashMap). If it is, return it. If not, compute it, store it in the Cache, and return it.
- Pros: Very intuitive. You just write the brute force recursive logic and add 3 lines of caching code.
- Cons: Function call overhead. Deep recursion can cause
StackOverflowError.
Bottom-Up (Tabulation)
- How it works: Abandon recursion entirely. Use an array (
dp[]) and iterativeforloops. Start by filling the base cases (dp[0],dp[1]), then write a loop that calculatesdp[i]based on previously computed values. - Pros: Extremely fast. No recursion overhead. Allows for Space Optimization (dropping the array for a few variables).
- Cons: Can be harder to conceptualize for complex problems (like Interval DP).
4. Beginner Code Example: Fibonacci (The Hello World of DP)
Let's solve using both approaches.
Approach 1: Top-Down Memoization
public class FibonacciMemo {
public int fib(int n) {
// Initialize cache with -1 (meaning uncalculated)
int[] memo = new int[n + 1];
java.util.Arrays.fill(memo, -1);
return calculateFib(n, memo);
}
private int calculateFib(int n, int[] memo) {
// 1. Base cases
if (n <= 1) return n;
// 2. Cache check
if (memo[n] != -1) return memo[n]; // We already solved this!
// 3. Compute and Store
memo[n] = calculateFib(n - 1, memo) + calculateFib(n - 2, memo);
return memo[n];
}
}
Approach 2: Bottom-Up Tabulation
public class FibonacciTab {
public int fib(int n) {
if (n <= 1) return n;
// 1. Create DP table
int[] dp = new int[n + 1];
// 2. Initialize Base Cases
dp[0] = 0;
dp[1] = 1;
// 3. Loop and build
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
5. Intermediate Concept: The "FAST" Method for Solving DP
If you are stuck on a DP problem in an interview, do not try to jump straight to the optimal Bottom-Up loop. Use the FAST method:
- F - Find the First Solution: Write the purely recursive brute-force Backtracking solution. Identify the variables that change during recursion (these are your "State" variables).
- A - Analyze the Subproblems: Draw a quick recursion tree on the whiteboard. Do you see the exact same function calls repeating? Yes? Excellent.
- S - Subproblem Caching (Memoization): Add an array or HashMap. Before doing the recursive work, check the cache. After doing the work, write to the cache. You now have an optimal Top-Down DP solution!
- T - Turn it around (Tabulation): Look at your recursive function. The base cases become the
dp[0]initializations. The recursive step becomes the inside of aforloop. Write the Bottom-Up solution.
6. Common Mistakes Beginners Make
-
Caching an Object or Array
- The Mistake: Trying to use a full
List<Integer>as the key in a Memoization HashMap. - Why it's wrong: Hashing large objects is incredibly slow ( per hash). Your DP state should ideally be 1, 2, or 3 simple primitives (like integers
index,weight,count). If you can map your state to primitive integers, you can use a primitive arrayint[][] dpinstead of a HashMap, making lookups literally instant.
- The Mistake: Trying to use a full
-
Mixing Top-Down and Bottom-Up mental models
- The Mistake: Trying to write a
forloop inside a recursive function for standard 1D DP. - The Fix: Pick one lane. Either write a pure recursive function (Top-Down) or a pure iterative loop (Bottom-Up). Mixing them leads to chaotic state management.
- The Mistake: Trying to write a
-
Returning too early in recursion
- The Mistake:
if (memo[n] != 0) return memo[n];when0is actually a valid mathematical answer for the problem. - The Fix: Always initialize your memoization arrays with a value that is impossible for the problem to output (like
-1orInteger.MIN_VALUE).
- The Mistake:
7. Interview Q&A
Q1: What's the difference between Divide & Conquer and Dynamic Programming? Answer: Both break a problem into subproblems. However, Divide & Conquer subproblems are strictly independent (they never overlap, like the left and right halves of Merge Sort). DP subproblems overlap massively (they share the same sub-trees). If you use D&C on overlapping subproblems, you get catastrophic time complexity.
Q2: Should I always optimize the space of a Bottom-Up DP?
Answer: In an interview, always write the standard space array first to ensure your logic is correct. Then, tell the interviewer: "I notice that dp[i] only ever looks at dp[i-1]. I can optimize this to space by just using two variables." This demonstrates deep understanding without risking messing up the initial code.
Q3: Why is Bottom-Up DP generally faster than Top-Down if the Big-O time complexity is identical?
Answer: Top-Down recursion requires placing a "stack frame" on the call stack for every function call. This involves memory allocation and pointer jumping, which has a high constant-time overhead. Bottom-Up is a pure for loop updating a contiguous array in memory, which is aggressively optimized by the CPU cache.
8. Practice Problems
- Fibonacci Number (LeetCode 509)
- Goal: Implement both Memoization and Tabulation perfectly.
- Climbing Stairs (LeetCode 70)
- Goal: Realize that this is literally just Fibonacci disguised as a word problem.
- N-th Tribonacci Number (LeetCode 1137)
- Goal: Extend your 1D logic.
T(n) = T(n-1) + T(n-2) + T(n-3).
- Goal: Extend your 1D logic.
9. Summary
- DP = Recursion + Caching. It's an optimization technique, not magic.
- Top-Down (Memoization): Recursive, easy to write, high overhead.
- Bottom-Up (Tabulation): Iterative, slightly harder to conceptualize, blazing fast execution.
- State: The variables that define a unique subproblem. Finding the minimum number of primitive variables to represent your state is the secret to mastering DP.
Advanced Code Implementations
// Example: Coin Change (Bottom-up DP)
import java.util.Arrays;
public class CoinChange {
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];
}
}
// Example: Longest Increasing Subsequence
public class LIS {
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int max = 1;
for(int i=0; i<nums.length; i++) {
dp[i] = 1;
for(int j=0; j<i; j++) {
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}
Expanded Interview Q&A
Q1: What are the two main properties of a problem that suggest DP?
Answer: Overlapping Subproblems and Optimal Substructure.
Q2: What is the difference between Memoization and Tabulation?
Answer: Memoization is top-down (recursive with caching), while Tabulation is bottom-up (iterative table filling).
Q3: How do you identify the state in a DP problem?
Answer: The state is defined by the parameters needed to uniquely identify a subproblem, such as an index, a remaining capacity, or a mask.
Q4: Why is Coin Change initialized with amount + 1?
Answer: Because amount + 1 represents infinity (an impossible number of coins), allowing Math.min to correctly find the smallest count.
Q5: Can DP be used for finding all paths instead of just the optimal one?
Answer: Generally no. DP is for optimization (min/max) or counting. To find all paths, backtracking/DFS is required.