Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Greedy, Backtracking & Dynamic Programming  ›  Lesson

Memoization & the Slot Filler

Dynamic Programming 35 minRemember each subproblem's answer so it's only ever solved once
You're building a piece ofTunebox — festival planner
This piece — count_fills(): Counts the ways to fill a time slot — without recomputing.
Scenario The Tunebox Radio scheduler shows how many ident sequences could fill each gap between shows. Long gaps have trillions, so the count has to come from remembered subproblems, not from listing them.
Your task
Build count_fills(slot, lengths). Count the sequences of idents (any length in lengths, repeats allowed, order matters) whose lengths add up to exactly slot minutes. count_fills(0, lengths) is 1, the empty sequence. Memoize it, because an hour-long slot has trillions of sequences. Example: count_fills(4, [1, 2]) → 5.

Memoization & the Slot Filler

Tunebox Radio fills gaps between shows with station idents, and the scheduling screen shows how many different ident sequences could fill each gap. The obvious recursive count answers instantly for a 10-minute gap and freezes the app at 40. The recursion isn't wrong. It's answering the same questions millions of times. Memoization makes it remember.

The slow way: Fibonacci

Fibonacci is the smallest program with this disease. Count the calls:

python

21,891 calls to work out one number. Try a small one first:

What does this print?
calls = 0
def fib(n):
    global calls
    calls += 1
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

fib(5)
print(calls)

Overlapping subproblems

Here is the call tree for fib(5):

                     fib(5)
              /                \
          fib(4)               fib(3)
         /      \             /      \
     fib(3)    fib(2)      fib(2)   fib(1)
     /    \     /   \      /   \
 fib(2) fib(1) f(1) f(0) f(1) f(0)
  /  \
f(1) f(0)

fib(3) is solved twice and fib(2) three times. Every level roughly doubles the repeats, so the running time grows like φⁿ ≈ 1.618ⁿ. fib(40) makes about 331 million calls. When a problem has overlapping subproblems (the same smaller question keeps coming up) and optimal substructure (the answer is built from answers to smaller questions), it's a dynamic programming problem. The fix is to solve each question once.

Memoize with a dict

Before computing, check whether you already know the answer. After computing, write it down:

python

Step through fib(4). The second time fib(2) is asked for, it comes straight out of memo:

step through it
1memo = {}
2def fib(n):
3 if n in memo:
4 return memo[n]
5 if n < 2:
6 return n
7 memo[n] = fib(n - 1) + fib(n - 2)
8 return memo[n]
9 
10print(fib(4), memo)
What does this print?
calls = 0
def fib(n, memo):
    global calls
    calls += 1
    if n in memo:
        return memo[n]
    if n < 2:
        return n
    memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
    return memo[n]

fib(30, {})
print(calls)

The cache key must hold everything that matters

A memo is only correct if the same key always means the same answer. Here the memo remembers answers for one set of ident lengths and serves them for a different set:

broken — fix it

With 1- and 3-minute idents there are 3 ways to fill 4 minutes, so this should print 5 3. It prints 5 5, because the second call reads answers cached by the first.

❓ Cross-question: "Isn't memoization just caching?" Yes: a cache of a function's results, keyed on its arguments. That only works for a pure function, one whose answer depends on its arguments and nothing else. Read a global, or leave out an argument from the key, and the cache serves stale answers without any error.

functools.cache: memoization in one line

The standard library writes the dict for you. @cache (Python 3.9+) is shorthand for @lru_cache(maxsize=None):

python

The cache stores arguments as dict keys, so every argument must be hashable:

What happens when this runs?
from functools import cache

@cache
def total_minutes(lengths):
    return sum(lengths)

print(total_minutes([3, 4]))

Counting ways to fill a slot

With 1- and 2-minute idents, any fill of t minutes ends with either a 1-minute ident (after a fill of t − 1) or a 2-minute ident (after a fill of t − 2). So:

  • ways(t) = ways(t − 1) + ways(t − 2)
  • ways(0) = 1, because there's exactly one way to fill nothing: play nothing.

That's Fibonacci again, and it's the same recurrence as climbing stairs one or two steps at a time.

python

For any list of lengths, the last ident can be any length L, so you add up ways(t − L) over every length. That's your task.

Sequences or combinations?

1 + 2 and 2 + 1 are two different broadcasts, so count_fills counts sequences. If you only care which idents you use, not their order, you're counting combinations, and you need more state:

python

To count combinations directly, stop the recursion from picking an earlier length after a later one. The state then needs a second number, which lengths you may still use, so the memo key becomes (index, remaining). General rule: the state is every argument that changes between calls.

The top-down recipe:

  1. Write the plain recursion. Get it correct on small inputs.
  2. Name the state: the arguments that change between calls.
  3. Cache on exactly that state, with a dict or @cache.
Version Time Space
Plain recursive fib(n) O(φⁿ) ≈ O(1.618ⁿ) O(n) call stack
Memoized fib(n) O(n) O(n) memo + stack
Memoized count_fills(slot, lengths) O(slot · k) for k lengths O(slot)
One memo lookup or store O(1) average none

Coming from Java/JS: the memo is a HashMap<Integer, Long> or a JS Map. One real difference: Python integers never overflow. Java's long overflows at fib(93), and a JS number stops being exact after 2⁵³, at fib(79). Python just keeps going.


Idioms & real-world patterns

A fresh cache per call

Decorate a nested function, and every call of the outer function gets a new cache. Inputs that never change, like an unhashable list, stay outside the key, and nothing leaks between calls. Here Tunebox promotes tracks on the home page, but never two neighbours in a row:

python

⚡ Advanced: the recursion limit

A cache hit returns immediately, but a cache miss still goes one frame deeper. Python stops at about 1000 frames, so asking a cold cache for ways(5000) raises RecursionError. The workaround is to warm the cache in small steps, so no single call has far to fall:

python

Taken to its conclusion, filling answers from small t upward with no recursion at all is tabulation, the next lesson.

⚡ Advanced: bounded caches

@lru_cache(maxsize=128) keeps only the 128 most recently used answers. That's right for a web handler seeing endless new arguments, and wrong for DP, where evicting a subproblem means solving it again. fib.cache_clear() empties a cache between runs.


🎯 Your turn

Write count_fills(slot, lengths). It counts the sequences of idents (any length in lengths, repeats allowed, order matters) whose lengths add up to exactly slot minutes. count_fills(0, lengths) is 1: the empty sequence.

  • count_fills(4, [1, 2])5 (1+1+1+1, 1+1+2, 1+2+1, 2+1+1, 2+2)
  • count_fills(60, [1, 2])2504730781961, which the checker only waits for if you memoize

Hint: ways(t) = sum(ways(t − L) for L in lengths if L <= t), with ways(0) = 1. Cache on t.

Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green means this piece of the app is built. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upFibonacci, remembered

Write fast_fib(n) returning the n-th Fibonacci number, where fast_fib(0) is 0, fast_fib(1) is 1, and every later number is the sum of the two before it. Memoize it, so fast_fib(70) returns instantly.

memoizationbase-case
DrillRoutes to the main stage

The festival site is a grid of rows × cols blocks. You enter at the top-left block, and the main stage is in the bottom-right block. Every step moves one block right or one block down. Write route_count(rows, cols) returning how many different routes reach the stage. route_count(2, 3)3.

memoizationlru-cache
BuildCombinations, not sequences

Write fill_combos(slot, lengths). Like count_fills, but order doesn't matter: 1+2 and 2+1 are the same fill. Count the combinations of ident lengths (distinct positive numbers, each usable any number of times) that add up to exactly slot. fill_combos(4, [1, 2])3 (1+1+1+1, 1+1+2, 2+2). fill_combos(0, lengths) is 1.

memoizationdp
BossSplit the hashtag

Tunebox turns hashtags into search words. Write count_splits(tag, words) returning the number of ways to cut tag into pieces that are all in words (a word may be used more than once). count_splits("lofichillbeats", ["lo", "fi", "lofi", "chill", "beats", "chillbeats"])4. An empty tag has exactly one split: no pieces.

memoizationstringsslicing
CapstoneSync two playlists

Two devices hold different versions of the same playlist. Write sync_cost(old, new) returning the fewest single-track edits that turn old into new. An edit is inserting one track, deleting one track, or replacing one track with another. sync_cost(["Aurora", "Blink", "Cobalt"], ["Aurora", "Cobalt"])1.

memoizationlru-cachedp
count_fills.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
count_fills(slot, lengths) → intHow many ident sequences fill the slot exactly.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.