Dynamic Programming (DP) is frequently cited as one of the most challenging topics in computer science and technical interviews. Yet, at its core, dynamic programming is simply an optimization technique for solving complex problems by breaking them down into simpler, overlapping subproblems.
This guide demystifies DP by taking you through the systematic process of identifying dynamic programming problems, writing brute-force solutions, applying memoization, converting to bottom-up tabulation, and finally optimizing space complexity down to O(1).
1. The Core Pillars of Dynamic Programming
Before writing code, a problem must satisfy two fundamental properties to be solvable via dynamic programming:
- Optimal Substructure: An optimal solution to the problem can be constructed from optimal solutions of its subproblems. If you can express the solution of a problem as a recurrence relation combining solutions to its sub-parts, it has optimal substructure.
- Overlapping Subproblems: The recursive algorithm visits the same subproblems repeatedly rather than generating new subproblems constantly. If a recursive tree evaluates the exact same parameters multiple times, DP can cache these results to avoid redundant work.
2. The Classic Benchmark: The Fibonacci Sequence
While mathematically trivial, the Fibonacci sequence serves as the canonical example for illustrating the progression from recursion to optimized DP.
The Naive Recursive Approach
The mathematical definition of Fibonacci is:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) for n > 1
Directly translating this into code yields an exponential time complexity of $O(2^n)$:
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)
As n grows, the call tree branches out exponentially, calculating fib_naive(2) or fib_naive(3) millions of times.
3. Top-Down Approach: Memoization
To fix exponential redundancy, we introduce Memoization (Top-Down DP). We keep a cache (an array or hash map) initialized with null values. When a subproblem is solved, its result is stored in the cache. When encountered again, the cached value is returned immediately.
Python Implementation
def fib_memoization(n, memo=None):
if memo is None:
memo = {}
if n <= 1:
return n
if n in memo:
return memo[n]
# State transition equation
memo[n] = fib_memoization(n - 1, memo) + fib_memoization(n - 2, memo)
return memo[n]
- Time Complexity: $O(n)$, because each subproblem from $0$ to $n$ is calculated exactly once.
- Space Complexity: $O(n)$ for the memoization hash map/array and the recursive call stack.
4. Bottom-Up Approach: Tabulation
While memoization is intuitive, it still incurs overhead from the recursive call stack. Tabulation (Bottom-Up DP) eliminates recursion entirely. We iteratively build the solution from the base cases up to the target value n using an array.
Transforming Recursion to Iteration
- Initialize a DP table of size
n + 1. - Set the base cases explicitly.
- Loop from the smallest subproblem to the largest, filling the table using the state transition relation.
Python Implementation
def fib_tabulation(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
- Time Complexity: $O(n)$
-
Space Complexity: $O(n)$ due to the
dparray.
5. Space Optimization: Dropping Dimensions
Look closely at the tabulation equation for Fibonacci: dp[i] = dp[i - 1] + dp[i - 2]. To compute the value at index i, we only require the results from the two preceding indices (i-1 and i-2). We do not need the results from indices 0 through i-3.
We can completely discard the dp array and maintain only two variables representing the previous two states.
Optimized Python Implementation
def fib_optimized(n):
if n <= 1:
return n
prev2 = 0
prev1 = 1
current = 0
for _ in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return current
- Time Complexity: $O(n)$
- Space Complexity: $O(1)$ — constant auxiliary space.
6. Case Study: 0/1 Knapsack Problem
Let us apply this methodology to a classic multi-variable DP problem: The 0/1 Knapsack Problem.
Problem Statement
Given weights and values of n items, put these items in a knapsack of capacity W to get the maximum total value in the knapsack. You cannot fractionally break an item; you must take it entirely or leave it.
1. State Definition
Let dp[i][w] represent the maximum value that can be attained considering the first i items and a maximum weight capacity w.
2. State Transition
For each item i, we have two choices:
-
Exclude the item: The value is the same as considering
i-1items with weightw:dp[i-1][w]. -
Include the item (if weight of item
iis less than or equal tow): The value is the value of itemiplus the optimal value of the remaining capacityw - weight[i]using the previous items:value[i] + dp[i-1][w - weight[i]].
$$\text{dp}[i][w] = \max(\text{dp}[i-1][w], \text{value}[i] + \text{dp}[i-1][w - \text{weight}[i]])$$
Tabulated Implementation (Java)
public class Knapsack {
public static int knapsack(int capacity, int[] weights, int[] values, int n) {
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 0; i <= n; i++) {
for (int w = 0; w <= capacity; w++) {
if (i == 0 || w == 0) {
dp[i][w] = 0;
} else if (weights[i - 1] <= w) {
dp[i][w] = Math.max(values[i - 1] + dp[i - 1][w - weights[i - 1]], dp[i - 1][w]);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
return dp[n][capacity];
}
}
- Time Complexity: $O(n \times W)$
- Space Complexity: $O(n \times W)$ for the 2D table.
Space Optimization for Knapsack
Notice that dp[i][w] only ever relies on the previous row dp[i-1]. We can optimize the 2D matrix into a 1D array of size W + 1.
public static int knapsackOptimized(int capacity, int[] weights, int[] values, int n) {
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++) {
// Iterate backwards to avoid using updated values from the same row
for (int w = capacity; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], values[i] + dp[w - weights[i]]);
}
}
return dp[capacity];
}
- Time Complexity: $O(n \times W)$
- Space Complexity: $O(W)$ — reducing the space footprint dramatically from matrix dimensions to a single capacity array.
Summary Checklist for Solving DP Problems
- Identify the Objective: What is the final question asking for? (Min cost, max profit, number of ways).
-
Define the State: Write down parameters that uniquely identify a subproblem (e.g., index
i, remaining capacityw). -
Formulate the Recurrence: Relate
dp[state]to smaller subproblem states. - Write Memoized Recursion: Implement top-down with a cache to verify correctness.
- Convert to Tabulation: Rewrite iteratively to remove stack overflow risks.
- Analyze Dependencies: Check if previous rows or elements can be dropped to achieve optimal space complexity.

Top comments (0)