Hello! Let's continue our journey into algorithm design.
Introduction
In our previous lesson, we established the core principles of Dynamic Programming (DP). You learned that a problem is a good candidate for DP if it has optimal substructure and, crucially, overlapping subproblems. We saw how a naive recursive solution for the Fibonacci sequence becomes incredibly slow because it repeatedly calculates the same values, a classic sign of overlapping subproblems.
Today, we'll move from theory to practice. You will learn the two primary techniques for implementing a DP solution to fix this inefficiency. Your learning outcome for this lesson is to: Solve the Fibonacci sequence problem using both memoization (top-down) and tabulation (bottom-up) DP.
By the end of this lesson, you will be able to write efficient Python code for the Fibonacci sequence and understand the trade-offs between these two powerful DP approaches. This is a fundamental skill that you'll build upon to solve more complex DP problems required for the GATE exam.
First, let's visualize the two paths we're about to take.

The Problem Revisited: Inefficient Recursion
As a quick refresher, let's look at the simple, but inefficient, recursive function for the Fibonacci sequence.
def fib_recursive(n):
# Base cases
if n <= 1:
return n
# Recursive step with overlapping subproblems
return fib_recursive(n - 1) + fib_recursive(n - 2)
# Trying to calculate fib_recursive(40) would take a very long time!
As we discussed, the issue is that fib_recursive(n-1) and fib_recursive(n-2) lead to a tree of function calls with many redundant branches. For example, calculating fib_recursive(5) ends up calculating fib_recursive(2) three separate times.

This leads to an exponential time complexity of . Now, let's see how DP solves this.
1. Memoization: The Top-Down Approach
Memoization is a technique where you store the results of expensive function calls and return the cached result when the same inputs occur again. It's like adding a memory to our recursive function.
This is called a top-down approach because you start with the original problem (e.g., fib(n)) and break it down recursively. The magic happens when a subproblem is encountered for the second time; instead of re-computing, you just look up the answer.
To understand how to convert a recursive solution into a memoized one, let's watch a detailed explanation.
DP 1. Introduction to Dynamic Programming | Memoization | Tabulation | Space Optimization Techniques
The video 'Introduction to Dynamic Programming' from the 'take U forward' channel provides an excellent, step-by-step guide on implementing memoization.
Watch from the beginning up to 14:40. The presenter first recaps the recursive tree, identifies the overlapping subproblems, and then clearly explains the logic and steps to add a 'DP array' (a cache) to the recursive solution.
Steps to Implement Memoization:
As the video explained, you can convert almost any pure recursive function to a memoized one with these steps:
- Create a cache: Create a data structure (like an array or a dictionary) to store the results of subproblems. Initialize it with a sentinel value (e.g., -1) to indicate that a subproblem hasn't been solved yet.
- Check the cache: At the beginning of your function, before any computation, check if the solution for the current state is already in the cache. If it is, return the cached value immediately.
- Store the result: If the solution was not in the cache, compute it recursively as before. Then, just before returning the result, store it in the cache.
Here is what this looks like in Python:
# A cache (or dp array) to store Fibonacci numbers
# We can initialize it with -1 to indicate 'not yet computed'
n = 10
dp = [-1] * (n + 1)
def fib_memo(n):
# Base cases
if n <= 1:
return n
# 1. Check if the result is already in our cache
if dp[n] != -1:
return dp[n]
# 2. If not, compute it recursively
result = fib_memo(n - 1) + fib_memo(n - 2)
# 3. Store the result in the cache before returning
dp[n] = result
return result
print(f"The {n}th Fibonacci number is: {fib_memo(n)}")
# This would now calculate fib(40) almost instantly.
Complexity Analysis (Memoization)
- Time Complexity: . Each Fibonacci number from 2 to
nis computed only once. Subsequent calls are lookups. - Space Complexity: . This comes from two sources: the size of our
dparray () and the depth of the recursion stack, which can go up tonin the worst case ().
Test your understanding!
Imagine you call fib_memo(4). List the sequence of function calls. Which of these calls will result in a cache lookup (returning a stored value) versus a full computation?
Initial state: dp = [-1, -1, -1, -1, -1]
Show answer
fib_memo(4): Not in cache. Computes. Callsfib_memo(3).fib_memo(3): Not in cache. Computes. Callsfib_memo(2).fib_memo(2): Not in cache. Computes. Callsfib_memo(1).fib_memo(1): Base case. Returns1.fib_memo(2)now callsfib_memo(0).fib_memo(0): Base case. Returns0.fib_memo(2)finishes computation (1 + 0 = 1), storesdp[2] = 1, and returns1.fib_memo(3)now callsfib_memo(1).fib_memo(1): Base case. Returns1.fib_memo(3)finishes computation (1 + 1 = 2), storesdp[3] = 2, and returns2.fib_memo(4)now callsfib_memo(2).fib_memo(2): Is in cache!dp[2]is1. Lookup. Returns1immediately.fib_memo(4)finishes computation (2 + 1 = 3), storesdp[4] = 3, and returns3.
The only cache lookup was the second call to fib_memo(2).
2. Tabulation: The Bottom-Up Approach
Tabulation takes the opposite approach. Instead of starting from n and going down, it starts from the base cases and builds the solution iteratively up to n. It fills a "table" (usually an array) from the beginning.
This is called a bottom-up approach because you solve the smallest subproblems first and use their results to build up solutions to larger subproblems. For Fibonacci, this is very intuitive: calculate fib(2), then fib(3), then fib(4), and so on, until you reach fib(n).
To see how this is done, let's go back to our guide.
Dynamic Programming Introduction - Tutorial
The article 'Dynamic Programming Introduction' from takeuforward.org (the text version of the video) explains the tabulation approach clearly.
Read 'Part -2: Tabulation'. It outlines the steps and provides a Python code example showing how to build the solution from the base cases up.
Steps to Implement Tabulation:
- Create a table: Create a
dparray of sizen+1to store the results. - Initialize base cases: Manually fill in the solutions for the base cases in the table (e.g.,
dp[0] = 0,dp[1] = 1). - Iterate and fill: Loop from the first unsolved subproblem up to
n. In each iteration, calculatedp[i]using the already-computed values in the table (dp[i-1]anddp[i-2]). - Return the final answer: The solution to the original problem is the last value computed:
dp[n].
Here's the Python implementation:
def fib_tabulation(n):
# Handle base cases for n=0 and n=1
if n <= 1:
return n
# 1. Create the table
dp = [0] * (n + 1)
# 2. Initialize base cases
dp[1] = 1
# 3. Iterate from 2 to n and fill the table
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
# 4. The answer is at the end of the table
return dp[n]
n = 10
print(f"The {n}th Fibonacci number is: {fib_tabulation(n)}")
Complexity Analysis (Tabulation)
- Time Complexity: . We simply loop from 2 to
nonce. - Space Complexity: . We use an array of size
n+1to store the results. Notice that unlike memoization, there is no extra space used by a recursion stack.
3. Space Optimization
If you look closely at the tabulation loop, you'll see a key detail: to compute dp[i], we only need dp[i-1] and dp[i-2]. We don't need dp[i-3], dp[i-4], etc. So, why are we storing the entire array?
We can optimize the space complexity from down to by only keeping track of the last two values.
Let's see how this final optimization is done.
DP 1. Introduction to Dynamic Programming | Memoization | Tabulation | Space Optimization Techniques
Let's return to the 'take U forward' video, which concludes by demonstrating this clever space optimization.
Watch from 27:13 to 30:00. Pay attention to how the dp array is replaced by just two variables that get updated in each step of the loop.
Here's the space-optimized code in Python:
def fib_optimized(n):
if n <= 1:
return n
# We only need the previous two numbers
prev2 = 0 # Represents fib(i-2)
prev1 = 1 # Represents fib(i-1)
# Loop from 2 to n
for _ in range(2, n + 1):
current = prev1 + prev2
# Update pointers for the next iteration
prev2 = prev1
prev1 = current
return prev1 # prev1 holds the final fib(n)
n = 10
print(f"The {n}th Fibonacci number is: {fib_optimized(n)}")
Complexity Analysis (Space-Optimized)
- Time Complexity: . The loop still runs
n-1times. - Space Complexity: . We only use a few variables (
prev2,prev1,current), regardless of the size ofn. This is the most efficient solution for this problem.
Conclusion
Congratulations! You've successfully implemented your first Dynamic Programming problem using three different approaches. This is a huge step in building your algorithmic toolkit.
Key Takeaways:
- Memoization (Top-Down): A recursive approach that adds a cache to avoid recomputing subproblems. It's often more intuitive to write if you already have a recursive solution.
- Time: | Space: (for cache + recursion stack)
- Tabulation (Bottom-Up): An iterative approach that builds the solution from the smallest subproblems up. It avoids recursion overhead.
- Time: | Space: (for the table)
- Space Optimization: For problems like Fibonacci, where the solution only depends on a few previous results, we can optimize the tabulation approach to use constant space.
- Time: | Space:
Preview of the Next Lesson:
We've now seen how DP thoroughly explores subproblems to guarantee an optimal solution. In our next lesson, we will explore a different algorithmic paradigm: Greedy Algorithms. We will learn how a greedy algorithm makes the "best-looking" choice at each step, hoping to find a global optimum. We will contrast this with DP and learn to identify when a simpler, faster greedy approach is sufficient by applying it to the Activity Selection problem.