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.
- 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.
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 returningThe 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.