Code of the Day
AdvancedDynamic Programming

Memoisation

Add a memo dict to recursive Fibonacci, then use @lru_cache as the Pythonic shorthand, and watch call counts drop from exponential to linear.

CS FundamentalsAdvanced10 min read
Recommended first
By the end of this lesson you will be able to:
  • Add a memo dict to recursive Fibonacci to reduce calls from O(2^n) to O(n)
  • Use @lru_cache as the Pythonic equivalent of a manual memo dict
  • Trace the call tree before and after caching to confirm the reduction

Top-down DP is the simplest form of memoisation: write the recursion you already understand, then add one cache lookup and one cache store. Everything else stays the same.

Three implementations side by side

The code below defines three versions of Fibonacci. Each uses a call counter so you can compare exactly how many recursive calls each approach makes for the same input. Run it as-is, then try increasing N to 35 or 40.

Python — editable, runs in your browser

For N = 20, fib_naive makes over 21 000 calls. Both memoised versions make exactly 21 calls — one per unique input from 0 to 20.

Why the memo dict works

fib_memo adds two lines to the naive version:

if n in memo:          # cache hit — return immediately
    return memo[n]
# ... compute ...
memo[n] = result       # cache miss — store before returning

The first time fib_memo(3) runs, it computes its answer and stores it in memo[3]. The second time — triggered by a different branch of the recursion — the if n in memo check returns immediately without recursing further. Every unique argument is computed at most once.

@lru_cache: the Pythonic shorthand

functools.lru_cache does the same thing with a decorator. The cache is keyed by the function's arguments. maxsize=None means the cache is unbounded — it keeps all entries rather than evicting the least recently used ones. For typical DP problems this is what you want.

Python 3.9+ also provides @functools.cache (no maxsize argument, always unbounded) as a cleaner alias for @lru_cache(maxsize=None). Both are correct for DP; use whichever your team prefers.

One subtlety: @lru_cache requires the function's arguments to be hashable. Lists are not hashable, so if your sub-problem state includes a list you must convert it to a tuple first, or switch to a manual dict.

The memo dict and default mutable arguments

Notice that fib_memo takes memo=None and initialises it inside the function, rather than memo={} in the default argument. Python evaluates default argument values once at function definition time. If you wrote def fib_memo(n, memo={}), the same dict would be shared across all top-level calls, which can cause subtle bugs in other contexts — though for Fibonacci it would accidentally work. Use None and initialise inside as the safe pattern.

Where to go next

Next: tabulation — the bottom-up alternative that fills a table from base cases upward, avoiding Python's recursion limit entirely and enabling space optimisations that top-down caching cannot.

Finished reading? Mark it complete to track your progress.

On this page