Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 30

Fibonacci with DP: From Recursion to Memoization

Recommended resources

Hello! Welcome back to our journey into Dynamic Programming.

In our last lesson, we established the conceptual foundation for DP, identifying the two critical properties a problem must possess: optimal substructure and overlapping subproblems. We saw that the Fibonacci sequence is a perfect example of a problem with overlapping subproblems, making it a prime candidate for optimization.

Today, we will act on that observation. Your goal is to solve the Fibonacci sequence using Dynamic Programming, specifically by illustrating the transition from a simple recursive solution to a much more efficient one using a technique called memoization. This is our first practical step in implementing a DP solution.

1. The Naive Approach: Pure Recursion

Let's start with the most intuitive way to solve the Fibonacci problem, which directly translates its mathematical definition into code. The Fibonacci sequence is defined by the recurrence relation:

with the base cases and .

A direct translation of this into a recursive function is straightforward. The following reading provides a clear explanation and a simple Python implementation of this brute-force approach.

Solution 1: Brute Force Approach (Pure Recursion)

The article 'Nth Fibonacci Number using Dynamic Programming' on Code and Debug offers a great starting point. We'll begin with their first solution.

Read the 'Solution 1: Brute Force Approach (Pure Recursion)' section. Pay attention to the intuition of directly translating the mathematical formula into code and the 'Time and Space Complexity' analysis.

As the article points out, this solution is extremely inefficient. Its time complexity is . Why? Because it recalculates the same values over and over again. This is the "overlapping subproblems" issue we discussed in the last lesson manifesting as a major performance bottleneck.

Let's watch a short video that visualizes this inefficiency by tracing the recursion tree and deriving the exponential time complexity.

4 Principle of Optimality - Dynamic Programming introduction

This clip from Abdul Bari's popular series on dynamic programming clearly illustrates the problem.

Watch from 02:42 to 06:35. Notice how many times the function is called for the same value (e.g., fib(1), fib(2)). This visualization makes the cause of the exponential runtime very clear.

Fibonacci Recursion Tree
This diagram shows the redundant calls for `fib(4)`. The subproblem `fib(2)` is computed from scratch twice, and `fib(1)` is computed three times. This redundancy grows exponentially as `n` increases.

This is precisely the problem Dynamic Programming is designed to solve.


2. The Fix: Recursion with Memoization

The inefficiency comes from re-computing, so the solution is simple: don't re-compute.

Memoization (not "memorization") is the technique of storing the results of expensive function calls and returning the cached result when the same inputs occur again. It's a top-down approach to DP because we start from the top (the n we want to solve) and work our way down recursively, but we remember the solutions to subproblems along the way.

Essentially, we augment our recursive function with a "cache" (often an array or a hash map).

The logic becomes:

  1. Before computing F(k), check if we have already computed it and stored it in our cache.
  2. If yes, return the cached value instantly.
  3. If no, compute F(k) recursively, store the result in the cache, and then return it.

Let's see how this transforms our previous code.

Solution 2: Memoization Approach (Top-Down Dynamic Programming)

Let's return to the Code and Debug article, which explains this transition beautifully using a 'notebook' analogy.

Read the 'Solution 2: Memoization Approach (Top-Down Dynamic Programming)' section. Focus on how the code is modified to include the dp array (our cache) and the check if dp[num] != -1.

By adding this caching layer, we ensure that each Fibonacci number, F(k) for 0 <= k <= n, is computed exactly once.

The video we watched earlier demonstrates this optimization powerfully.

4 Principle of Optimality - Dynamic Programming introduction

Let's continue with the Abdul Bari video to see the dramatic effect of memoization on the recursion tree.

Now watch from 06:35 to 11:28. Observe how, after fib(2) is computed once, subsequent calls for fib(2) are skipped entirely. The video shows the number of calls dropping from 15 to just 6 for fib(5), illustrating the change from exponential to linear complexity.


3. A General Recipe for Memoization

This pattern of converting a pure recursive solution to a memoized one is a fundamental skill in DP. Since you have a software engineering background, having a repeatable process can be very useful. Here is a general three-step recipe:

  1. Create a data structure to act as a cache, usually an array or map. Initialize it with a sentinel value (like -1) to indicate that a subproblem has not yet been solved. The dimensions of this cache will match the parameters of your recursive function that define the subproblem state. For Fibonacci, this is just n, so a 1D array is sufficient.
  2. Add a check at the beginning of the function. Before any computation, check if the result for the current subproblem is already in the cache. If it is (i.e., not the sentinel value), return it immediately.
  3. Store the result before returning. If you had to compute the result because it wasn't in the cache, store this new result in the cache before you return it.

The following video succinctly summarizes this process.

DP 1. Introduction to Dynamic Programming | Memoization | Tabulation | Space Optimization Techniques

The channel 'take U forward' provides a very clear, step-by-step guide to converting a recursive solution into a memoized one.

Watch from 17:14 to 20:30. The video breaks down the conversion into three distinct steps: declaring a DP array, storing the computed answer, and checking for the answer before recursion. This is a practical template you can apply to other DP problems.


4. Analyzing the Improvement

Let's formally compare the two approaches. The "Summary" section of the Code and Debug article provides an excellent table for this.

Summary

To wrap up our comparison, let's look at a summary that puts everything side-by-side.

Read the 'Summary' section and review the comparison table. It clearly contrasts the time and space complexities of the different approaches.

Here's the breakdown:

Approach Time Complexity Space Complexity
Pure Recursion (for recursion stack)
Memoization (Top-Down DP) (for cache array + recursion stack)

With memoization, the time complexity drops from exponential to linear. This is a massive improvement. Each subproblem from to is solved only once. The space complexity remains linear, as we need an array of size n for our cache, in addition to the space used by the recursion stack.

A concise way to frame this, as noted in the MIT lecture slides on this topic, is:


Conclusion

In this lesson, you've taken a major practical step in learning Dynamic Programming. You've seen how to take an inefficient but intuitive recursive solution and, by applying the principle of memoization, transform it into an efficient one that runs in linear time.

Key Takeaways:

  • Purely recursive solutions to problems with overlapping subproblems, like Fibonacci, are often exponentially slow due to redundant computations.
  • Memoization is a top-down DP technique where you store the results of subproblems in a cache (e.g., an array or map).
  • By checking the cache before computing, you ensure that each subproblem is solved only once.
  • This technique transformed our Fibonacci algorithm from time complexity to .

Preview of the Next Lesson:
Memoization is a "top-down" approach. We started at n and recursively broke it down. There is another way: a "bottom-up" approach called tabulation. In the next lesson, we will see how to solve the Fibonacci problem iteratively, starting from the base cases and building our way up to n. This will complete our foundational understanding of the two main DP implementation strategies.