Skip to content

Intermediate: core algorithms · Recursion and backtracking · topic 25 of 61 · 35 min

Memoization: remembering answers

Explanation

Some recursive functions solve the same smaller problem again and again. Memoization stores each answer the first time it is computed (in a dictionary, or with functools.cache) and returns the stored answer on later calls. Naive Fibonacci drops from O(2ⁿ) to O(n). This is top-down dynamic programming, which Module 11 builds on.

The problem: the same work, again and again

The Fibonacci numbers start 0, 1 and each next number is the sum of the two before: 0, 1, 1, 2, 3, 5, 8, 13, … The recursive definition fib(n) = fib(n − 1) + fib(n − 2) is easy to write, but look at the calls for fib(5): it calls fib(4) and fib(3), and fib(4) calls fib(3) again. fib(2) is computed three times. The tree doubles at every level, so the time is O(2ⁿ) (Module 1 showed this recursion tree).

python
calls = 0
def fib(n):
    global calls
    calls += 1
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(30), calls)   # Output: 832040 2692537
Over 2.6 million calls for n = 30. fib(50) this way would make about 40 billion calls and take well over an hour.

Overlapping subproblems

A subproblem is a smaller version of the problem, such as fib(3) inside fib(5). The subproblems overlap when the same one is needed many times. Here there are only n + 1 different subproblems (fib(0) to fib(n)), but the naive code solves them millions of times. Compare this with merge sort in Topic 2: its halves never overlap, so remembering would not help there.

Memoization with a dictionary

Memoization means remembering: before computing, look the input up in a cache (a dictionary from input to answer). If it is there, return it. If not, compute it, store it, then return it. Each fib(k) is now computed once, so there are about 2n calls in total, each O(1): O(n) time. The cache holds n + 1 entries and the recursion is n deep, so the space is O(n).

python
def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n < 2:
        return n
    if n in memo:
        return memo[n]
    memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
    return memo[n]

print(fib_memo(90))   # Output: 2880067194370816120
Instant, because each value from 2 to 90 is computed once.

functools.cache and lru_cache

Python can add the cache for you. Put @functools.cache (Python 3.9 and later) or @functools.lru_cache(maxsize=None) above the function, and every result is stored by its arguments. lru_cache(maxsize=128) keeps only the 128 most recently used results, which saves memory but may recompute. The arguments must be hashable: numbers, strings and tuples work, lists do not (TypeError: unhashable type: 'list'). Call f.cache_clear() to empty the cache, for example between test cases.

python
from functools import cache

@cache
def climb(n):
    if n <= 1:
        return 1
    return climb(n - 1) + climb(n - 2)

print(climb(10))   # Output: 89
print(climb(45))   # Output: 1836311903
Ways to climb n stairs taking 1 or 2 steps at a time.

Climbing stairs and grid paths

Climbing stairs: you can take 1 or 2 steps at a time; how many ways reach step n? The last move was either 1 step (from n − 1) or 2 steps (from n − 2), so ways(n) = ways(n − 1) + ways(n − 2), with ways(0) = ways(1) = 1. It is Fibonacci in disguise. Grid paths from Topic 1 work the same way: paths(r, c) = paths(r − 1, c) + paths(r, c − 1). Without a memo, a 16 × 16 grid makes hundreds of millions of calls; with one, there are only 256 different (r, c) cells, so it is O(rows × cols). A blocked cell simply returns 0 paths.

Bridge to dynamic programming

Dynamic programming (DP) is the name for solving a problem from the answers of overlapping subproblems. Memoization is the top-down way: write the natural recursion and add a cache. The bottom-up way fills a table in a loop from the smallest subproblem upwards, which avoids the recursion limit and often needs less memory (Fibonacci needs only the last two values). Module 11 is all about DP; the habit to build now is to ask "do the same subproblems repeat?" whenever recursion is slow.

Common mistakes

  • Using a mutable default argument, def f(n, memo={}): the same dictionary is shared between ALL calls, even separate test cases with different inputs. Fix: memo=None and create it inside, or use @cache.
  • Memoizing on part of the state only, for example caching by r when the answer depends on (r, c). Fix: the cache key must include every argument that changes the answer.
  • Passing a list to an @cache function. Fix: pass a tuple, or keep the grid outside and cache on indexes only.
  • Expecting memoization to remove the recursion limit: fib_memo(5000) still goes 5000 deep and raises RecursionError. Fix: go bottom-up with a loop for large n.
  • Memoizing when subproblems do not repeat (merge sort, permutations). Fix: it only costs memory there; check for overlap first.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Memoization: remembering answers · 1/8

Memoization

This lesson shows how remembering answers can turn an impossibly slow recursion into a fast one.

Prefer a video by a teacher? Find free videos on “memoization fibonacci recursion lru_cache python explained overlapping subproblems” on YouTube ↗

In real life: Caching repeated questions in a fare calculator

A metro or bus fare app answers "what is the cheapest fare from station A to station B?" Many users ask the same pairs, and computing a route can mean exploring many smaller station-to-station pieces, which repeat across questions. Storing the answer for each pair (a cache keyed by the pair of stations) means each one is worked out once. Real systems also clear or update the cache when fares change, the same idea as cache_clear().

python
from functools import cache

fares = {("A", "B"): 10, ("B", "C"): 15, ("A", "C"): 30, ("C", "D"): 10, ("B", "D"): 30}
order = ["A", "B", "C", "D"]

@cache
def cheapest(src, dst):
    if src == dst:
        return 0
    best = float("inf")
    for (a, b), price in fares.items():
        if a == src and order.index(b) <= order.index(dst):
            best = min(best, price + cheapest(b, dst))
    return best

print(cheapest("A", "D"))
Shows 35: A to B (10), B to C (15), C to D (10). The answer for (B, D) and (C, D) is reused from the cache.

Practice (0/3 done)

Task 1 · Code · Python

Write fib_memo(n) that returns the nth Fibonacci number (fib(0) = 0, fib(1) = 1, fib(n) = fib(n − 1) + fib(n − 2)) using recursion WITH memoization: a dictionary or functools.cache. It must be fast for n = 90 (the plain recursion would take years). Inputs are 0 ≤ n ≤ 90. Example: fib_memo(10) returns 55.

Task 2 · Code · Python

You climb a staircase with n steps, taking either 1 or 2 steps at a time. Write climb_stairs(n) that returns the number of different ways to reach the top, using memoization so it runs in O(n). climb_stairs(0) is 1 (one way: do nothing). Inputs are 0 ≤ n ≤ 80. Example: climb_stairs(3) returns 3 (1+1+1, 1+2, 2+1).

Task 3 · Code · Python

grid is a list of rows; each cell is 0 (free) or 1 (blocked). A robot starts at the top-left cell and must reach the bottom-right cell, moving only RIGHT or DOWN, and may not enter a blocked cell. Write unique_paths_obstacles(grid) that returns the number of different paths, using recursion with memoization on (row, col), so it runs in O(rows × cols). Return 0 if the start or the end is blocked, and 0 for an empty grid []. The tests include an open 16 × 16 grid, which has 155,117,520 paths: far too many to walk one by one.

Quick quiz

Question 1

1.What is the time of fib(n) with memoization?

Question 2

2.Which problem gains nothing from memoization?

Question 3

3.Why can @functools.cache not be used on f(nums) when nums is a list?

Question 4

4.What is wrong with def f(n, memo={})?

Question 5

5.Your memoized fib_memo(5000) raises RecursionError. What is the best fix?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is the time of fib(n) with memoization?

Answer: O(n)

Each value from 0 to n is computed once, with O(1) work each.

Which problem gains nothing from memoization?

Answer: Merge sort, because its halves never repeat

Memoization only helps when the same subproblem is solved more than once.

Why can @functools.cache not be used on f(nums) when nums is a list?

Answer: Lists are not hashable, so they cannot be dictionary keys

The cache is a dictionary keyed by the arguments; lists raise TypeError: unhashable type. Pass a tuple instead.

What is wrong with def f(n, memo={})?

Answer: The same dictionary is shared by every call to f, even across separate inputs

Default values are created once when the function is defined. Use memo=None and create the dict inside.

Your memoized fib_memo(5000) raises RecursionError. What is the best fix?

Answer: Compute it bottom up with a loop

The memo removes repeated work but the recursion is still 5000 deep. A loop has no depth limit and needs only the last two values.

Which problem does NOT have overlapping subproblems?Related

Answer: Merge sort

Merge sort splits the list into halves that never repeat, so storing answers saves nothing.

From Dynamic programming: the idea

When is memoization unsafe?Related

Answer: For functions whose result depends on changing outside data

Cached results would be stale.

From Memoization, debounce and throttle

Why is naive recursive Fibonacci slow?Related

Answer: It recomputes the same values exponentially many times

fib(n-1) and fib(n-2) overlap heavily.

From Recursion

What is the extra space of a recursive function that goes n levels deep and creates no lists?Related

Answer: O(n)

n frames wait on the call stack at the deepest point, so the space is O(n).

From Recursion: functions that call themselves

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Recursion and backtracking”. The module exam comes next.
  2. Module 5 exam: Recursion and backtracking · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Sorting and searching. 61 topics left in the course before the final exam.

← Previous: Backtracking with pruning