Skip to main content

Welcome to a Grasp lesson

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

Lesson 38

Dynamic Programming: Memoization (Top-Down)

Recommended resources

Hello! Welcome to the first lesson in our new module, Dynamic Programming in Practice.

In the last module, we introduced Dynamic Programming (DP) as a powerful paradigm for solving problems that exhibit optimal substructure and overlapping subproblems. You learned to identify when DP is a suitable approach, contrasting it with Divide and Conquer and Greedy algorithms. Now, it's time to put that theory into practice.

Today's learning outcome is to implement a dynamic programming solution using memoization (top-down approach).

This technique is a cornerstone of algorithmic optimization. For you, as a developer aiming for an "architectural mindset," this is more than just coding; it's about learning a pattern to transform an inefficient, brute-force recursive algorithm into a highly performant one. This involves a deliberate trade-off between computation time and memory usage—a classic architectural decision.

1. Two Roads to Dynamic Programming: Top-Down vs. Bottom-Up

Dynamic Programming problems can generally be solved in two ways:

  1. Top-Down with Memoization: This approach feels very natural. You start with the original problem and break it down recursively. The "memoization" part means you store the results of subproblems as you solve them, so you never have to solve the same one twice.
  2. Bottom-Up with Tabulation: This approach is typically iterative. You start by solving the smallest possible subproblems and use their solutions to build up answers to larger and larger subproblems until you arrive at the solution for the original problem.
Visualizing Top-down vs. Bottom-up Dynamic Programming
A visual comparison of the Top-Down (recursive, breaking down) and Bottom-Up (iterative, building up) approaches in dynamic programming. We will focus on the Top-Down approach in this lesson.

2. The Problem with Naive Recursion: Fibonacci's Redundant Work

Let's use the classic Fibonacci sequence to see why a naive recursive solution is often a terrible idea. The definition is simple:

A direct translation into a recursive function seems straightforward. But what happens when we try to compute fib(5)? Let's see the explosion of redundant calculations.

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

This video from take U forward brilliantly illustrates the problem. It shows the recursion tree for calculating a Fibonacci number and highlights just how many times the same subproblems are computed over and over again.

Watch the segment from 05:52 to 12:22. Focus on the concept of 'overlapping subproblems'. Notice how many times f(3) and f(2) are re-calculated in the recursion tree for f(5).

As the video demonstrates, the number of function calls grows exponentially. The time complexity is , which becomes unfeasibly slow for even moderately large values of . This is the problem that memoization solves.


3. The Solution: Memoization

Memoization is a simple but powerful idea: "Don't re-compute what you've already solved."

We'll use a data structure—often an array or a hash map—to act as a "memo" or cache. When our function is asked to solve for a value n, it first checks the memo:

  • If the result for n is already there, it returns the cached value instantly.
  • If not, it computes the result, stores it in the memo, and then returns it.

This process effectively "prunes" the recursion tree, eliminating all redundant branches.

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

Let's continue with the same video to see how memoization transforms the inefficient recursive solution into an efficient one. It visually demonstrates how using a DP array prevents re-computation.

Now watch from 12:22 to 17:14. Pay attention to how the dp array is used to store and retrieve results, avoiding the recursive calls for subproblems that have already been solved.


4. A Step-by-Step Guide to Implementing Memoization

You can systematically convert almost any naive recursive solution for a DP problem into a memoized one. This recipe is a key takeaway for you to apply to other problems.

To get a clear understanding of these steps, watch the next part of the video.

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

The presenter now formalizes the process into three clear steps. This provides a practical 'recipe' you can follow to implement memoization yourself.

Watch from 17:14 to 20:33. Focus on understanding these three steps to convert a recursive solution into a memoized DP solution.

Here is a summary of the three key steps to transform a pure recursive function into a top-down DP solution:

  1. Create a memoization table: Create a data structure (e.g., an array or hash map) to store the results of the subproblems. The size should correspond to the possible states of your problem. Initialize it with a sentinel value (like -1 or null) to indicate that a state has not yet been computed.
  2. Add the memo check: At the top of your recursive function, immediately after the base cases, check if the solution for the current state is already in your memoization table. If it is (i.e., not the sentinel value), return the stored result immediately.
  3. Store the result: If the result was not in the memo, compute it recursively as before. Then, just before returning the result, store it in the memoization table.

Let's see this in pseudocode for our Fibonacci example:

// Step 1: Initialize memo table
memo = array of size n+1, filled with -1

function fib(n):
  // Base cases
  if n <= 1:
    return n

  // Step 2: Check the memo
  if memo[n] != -1:
    return memo[n]

  // Compute the result if not in memo
  result = fib(n-1) + fib(n-2)

  // Step 3: Store the result before returning
  memo[n] = result
  return result

Complexity Analysis

  • Time Complexity: . Each subproblem from fib(0) to fib(n) is computed exactly once. Subsequent calls are lookups.
  • Space Complexity: . We use an array of size n for the memoization table, and the depth of the recursion call stack will also be at most n.

We have traded memory for a massive reduction in computation time, from exponential to linear.

Test your understanding!

You are given the following naive recursive function to calculate the factorial of a number.

function factorial(n):
  if n == 0:
    return 1
  return n * factorial(n-1)

Is this problem a good candidate for memoization? Why or why not?

Show answer

No, it is not a good candidate. While it has optimal substructure (factorial(n) depends on factorial(n-1)), it lacks overlapping subproblems.

The recursion tree for factorial(5) would look like this: factorial(5) calls factorial(4), which calls factorial(3), and so on. No subproblem is ever calculated more than once. The naive recursive solution is already efficient with time complexity. Applying memoization would add space overhead (O(n)) without improving the time complexity. This is a crucial distinction to make before applying DP.


5. Generalizing the Pattern: Longest Common Subsequence (LCS)

The Fibonacci example uses a 1D memoization table. Let's look at a problem that requires a 2D table to generalize the pattern.

The Longest Common Subsequence (LCS) problem asks for the longest subsequence common to two sequences (e.g., two strings). For example, the LCS of "ABCBDAB" and "BDCABA" is "BCBA", which has a length of 4.

The first step, as always, is to define a recurrence relation.

Dynamic Programming I

The following document from Carnegie Mellon University provides a formal breakdown of DP problems. We'll use it to understand the LCS recurrence and then see how memoization is applied.

Read the section on the Longest Common Subsequence problem. First, focus on the initial exponential-time recursive program. Understand the logic: If the last characters match (S[n] == T[m]), the LCS length is 1 + LCS of the strings without their last characters. If they don't match, it's the max of either removing the last character from S or removing the last character from T. Then, examine the 'memoized version'. Notice how it's the exact same logic, but with the three-step memoization pattern we just learned, this time using a 2D memo table.

As you can see from the resource, the memoized solution for LCS follows the same pattern:

  1. Memo Table: A 2D array memo[n][m] is used because the subproblems are defined by two variables (the current lengths of the two strings).
  2. Check Memo: The function checks memo[n][m] at the start.
  3. Store Result: The computed result is stored in memo[n][m] before returning.

This reduces the time complexity from exponential to , at the cost of space for the memoization table.


Conclusion

In this lesson, you've learned the theory and practice of the top-down dynamic programming approach. You can now transform an inefficient, naive recursive algorithm into an optimized one using memoization.

Key Takeaways:

  • Memoization is a top-down DP technique that uses caching to avoid re-computing overlapping subproblems.
  • The implementation follows a simple three-step pattern: create a memo table, check the table at the start of your function, and store the result before returning.
  • This technique offers a significant time complexity improvement (often from exponential to polynomial) in exchange for increased space complexity. This time-space trade-off is a fundamental concept in system design and architecture.

Preview of the Next Lesson:

Now that you've mastered the top-down approach, what's next? In the next lesson, we will solve the same types of problems using the bottom-up approach, known as tabulation. You will learn to build your solution iteratively from the base cases up, often eliminating recursion entirely. This will give you another powerful tool for your algorithmic toolkit.