Skip to main content

Welcome to a Grasp lesson

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

Lesson 3

Memoization: Top-Down Dynamic Programming

Recommended resources

In the last lesson, we focused on the abstract modeling of dynamic programming problems by defining their state and the recurrence relation that links them. This process gives us the "blueprint" of a solution, equivalent to defining the vertices and edges of the subproblem dependency graph. Now, we will translate that blueprint into a working implementation.

This lesson covers the first of two primary implementation strategies: the top-down or memoized approach. You will learn how to implement a memoized solution directly from a recurrence relation. This method is often the most intuitive way to convert a DP model into code, as it closely mirrors the structure of a plain recursive solution.

From Recursion to Memoization

A dynamic programming recurrence, like , naturally suggests a recursive function. However, as we've discussed, a naive recursive implementation often leads to exponential runtimes due to re-computing the same subproblems repeatedly.

Memoization is a technique that optimizes this process by caching the results of expensive function calls. When the function is called again with the same inputs, it returns the cached result instead of re-computing it. For DP, this means we solve each unique subproblem exactly once.

This process is a general optimization pattern:

  1. Write a "brute-force" recursive function that directly implements the recurrence relation.
  2. Add a cache (e.g., a hash map or an array) to store the results of subproblems.
  3. In the recursive function, add a check at the beginning: if the solution for the current state is already in the cache, return it immediately.
  4. If not, compute the solution as before, but store it in the cache before returning.

The following video from MIT OpenCourseWare introduces this concept using the familiar Fibonacci sequence. It provides a clear illustration of transforming a naive recursive algorithm into a fast, memoized one and establishes the fundamental model for analyzing its performance.

Lecture 19: Dynamic Programming I: Fibonacci, Shortest Paths

Watch the initial segments of this lecture by Erik Demaine.

Focus on the following parts: Naive recursion: This section shows how a recurrence translates directly into a simple, but inefficient, recursive algorithm. Memoized DP algorithm: Pay close attention to the simple code transformation. A dictionary memo is added to cache results, drastically changing the performance. Complexity analysis: This is the key takeaway. The running time of a memoized DP algorithm is the (number of subproblems) × (work per subproblem, ignoring recursive calls). This simple formula is the foundation for analyzing all DP algorithms.

As the video explains, memoization ensures that the recursive body for each subproblem is executed only once. All subsequent calls for that same subproblem become constant-time lookups. This transforms an exponential-time exploration of a call tree into a linear-time traversal of the subproblem dependency DAG, where each node is visited just once.


Case Study: Coin Change

Let's apply this pattern to a more representative DP problem. The "Coin Change" problem asks if a target value V can be formed using a given set of coin denominations.

In the previous lesson, we would have modeled this as:

  • State: possible(v), a boolean indicating if value v can be made.
  • Recurrence: possible(v) = OR_{c in coins} possible(v-c) for all c <= v.
  • Base Case: possible(0) = true.

Now, we will implement it. The following resource from Carnegie Mellon University provides an excellent, structured walkthrough.

Dynamic Programming I

This document formalizes the entire process, from recurrence to efficient implementation. It will walk you through the thought process for the Coin Change problem.

Please read the following sections in order: Start with the problem setup to understand the goal. Next, review the recurrence, which formalizes the logic. Read the naive algorithm. This is the direct, but exponential, implementation of the recurrence. Notice its structural similarity to the Fibonacci example. Then, read the core idea of DP, which introduces memoization (top-down) as a way to avoid redundant computations shown in the call graph. Finally, study the top-down algorithm. This section presents the memoized code and is the direct implementation of this lesson's learning outcome. Observe how it matches the pattern described earlier: a cache (memoized array) is added, with a check at the start and a write at the end.

The document also provides a clear cost analysis for the top-down solution, arriving at a runtime of , where is the target value and is the number of coin denominations. This aligns perfectly with the formula from the MIT video:

  • Number of subproblems: (for values 0 to ).
  • Work per subproblem: (to iterate through the n coins).
  • Total Time: .

Memoization with Complex State: DP on Trees

The memoization pattern isn't limited to problems where the state is a simple integer index. It applies just as well to more complex states, such as nodes in a tree or tuples representing multiple parameters.

Recall the "Maximum Weight Independent Set on a Tree" problem from our last lesson (also known as "House Robber III"). We defined a refined state to solve it:

  • f(u): the max weight of an independent set in the subtree at u, including node u.
  • g(u): the max weight of an independent set in the subtree at u, excluding node u.

The recurrences were:

How do we implement the memoization cache for this? A simple array memo[i] is no longer sufficient if our nodes are objects in memory, not integers. This is a practical implementation detail that your background in runtime systems will appreciate. We need a way to map a subproblem (defined by a tree node u) to its solution (f(u) and g(u)).

The following article explores precisely this challenge.

Dynamic Programming Memoization with Trees

This article discusses the practical aspects of implementing a memoized DP solution on a tree structure.

Please read the following sections: First, review the section "Maximum-Weight Independent Sets in Trees" to re-familiarize yourself with the state definition. The article uses D_k for what we called f(k) (including node k) and D_bar_k for g(k) (excluding node k). Next, read through the derivation of the recurrences. This should be a review of the logic from our previous lesson. Finally, and most importantly, read the section "Memoization Storage in Trees". This section contrasts two implementation strategies. The "traditional" way requires a full traversal to map nodes to array indices. The "improved" way avoids this by either returning the two values (f(u), g(u)) as a pair from the recursive call or by augmenting the TreeNode structure itself. This latter approach effectively uses the graph structure itself as the memoization table.

The key insight is that the "memo" doesn't have to be an external data structure. By designing your recursive function to return the complete solution for a subproblem (e.g., a pair (f(u), g(u))), you implicitly pass the memoized values up the call stack. A parent u calls the function on its children v, receives the (f(v), g(v)) pair, and uses it to compute its own (f(u), g(u)) pair, which it then returns. The language's own function call stack manages the intermediate results, and if you use an explicit external cache (like a Map<TreeNode, SolutionPair>), you can prevent re-computation across disconnected parts of the call graph.


Summary

This lesson focused on translating a DP recurrence into a functional, efficient algorithm using top-down memoization.

  • The Pattern: Memoized DP is essentially "clever recursion." You start with a simple recursive function and augment it with a cache to store subproblem solutions, avoiding redundant work.
  • Direct Translation: This approach is a very direct translation of the mathematical recurrence into code, making it a natural first step when implementing a new DP solution.
  • Complexity Analysis: The performance is easy to analyze with the formula: Time = (Number of States) × (Work per State), where the work per state excludes the cost of recursive calls.
  • Implementation Matters: The choice of data structure for your memoization cache (array, hash map, or even the problem's own structure) is a practical consideration that depends on the nature of the state.

In our next lesson, we will explore the second major implementation strategy: bottom-up dynamic programming. Instead of starting from the top and recursively solving subproblems as needed, we will start with the base cases and iteratively compute the solutions to larger subproblems in a predetermined order—a process that directly corresponds to a topological sort of the subproblem DAG.