What is the Dynamic Programming Technique?
Dynamic Programming (DP) is used for solving complex problems by breaking them down into simpler overlapping subproblems and solving each subproblem only once, storing the result for future reuse. This avoids the overhead of recalculating solutions repeatedly (as in recursion).
DP is applicable when the problem has two main properties:
- Overlapping Subproblems: The problem can be broken into smaller subproblems that repeat over time.
- Optimal Substructure: The optimal solution of a problem can be constructed from the optimal solutions of its subproblems.
Example 1: Fibonacci Number — Explained for Beginners
Problem Statement:
Find the nth Fibonacci number, where the Fibonacci series is defined as follows:
F(0) = 0
F(1) = 1
F(n) = F(n - 1) + F(n - 2) for n ≥ 2
This means each number in the sequence is the sum of the two previous numbers. For example, the first few Fibonacci numbers are:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
Why Not Use Simple Recursion?
If we write a simple recursive solution like:
function fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
This looks clean, but it's extremely inefficient because it recalculates the same values again and again. For example, fib(5) will compute fib(4) and fib(3), but then fib(4) again computes fib(3) and fib(2) — so fib(3) is computed twice. The number of recursive calls grows exponentially.
This is where Dynamic Programming helps.
Dynamic Programming solves this problem by storing solutions to subproblems so we don’t compute them again. Let’s explore both top-down and bottom-up DP approaches:
Top-Down DP Approach (Memoization)
We use recursion, but we store the result of each Fibonacci number we calculate in a dictionary (called memo). Before computing any value, we check if it’s already in memo and use it directly if found.
Step-by-step Explanation:
- Start from
fib(n).
- Check if
n is already in memo. If yes, return it.
- If
n is 0 or 1, return n (base case).
- Otherwise, compute
fib(n-1) and fib(n-2).
- Store their sum in
memo[n].
- Return
memo[n].
Pseudocode
// Memoization approach
function fib(n, memo):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
Why It Works:
Each Fibonacci number is calculated only once and reused wherever needed. This avoids redundant calculations.
Time Complexity:
- O(n) — each number is computed once and stored.
Space Complexity:
Bottom-Up DP Approach (Tabulation)
Instead of starting from the top and going down recursively, we start from the bottom (base cases) and build up the solution iteratively.
Step-by-step Explanation:
- Create a table
dp[] of size n+1 to store Fibonacci values.
- Initialize:
dp[0] = 0 and dp[1] = 1.
- Iterate from
i = 2 to n.
- At each step, calculate
dp[i] = dp[i-1] + dp[i-2].
- Return
dp[n] as the final result.
Pseudocode
// Tabulation approach
function fib(n):
if n <= 1:
return n
dp = array of size (n+1)
dp[0] = 0
dp[1] = 1
for i from 2 to n:
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
Why It Works:
This method uses an iterative approach and solves the subproblems first before combining them to solve the main problem.
Time Complexity:
- O(n) — we compute each Fibonacci number from 2 to n once.
Space Complexity:
- O(n) — for the
dp array (can be optimized to O(1) by storing only last two values).
The Fibonacci number problem is a great way to understand dynamic programming. It highlights the idea of breaking a problem into smaller subproblems and using stored results to avoid recomputation. For beginners, this teaches how to recognize overlapping subproblems and how memoization and tabulation can drastically improve performance.