Some recursive solutions do the same work again and again. The Fibonacci numbers are the classic: each is the sum of the two before (0, 1, 1, 2, 3, 5, 8…). Written directly, fib(n) calls fib(n - 1) and fib(n - 2), which call smaller ones, and the same values get worked out millions of times. fib(40) makes over 300 million calls.
Dynamic programming fixes this by remembering. Memoization stores each answer the first time it's worked out (in a dictionary, say), and looks it up after that. In Python, the functools.cache decorator does it for you.
from functools import cache
@cache
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(40))
Prints
102334155
Now each fib(k) is worked out once, so the whole thing is O(n). The other style builds the answers bottom-up in a table, from the smallest case upwards, with a plain loop.
def fib_table(n):
table = [0, 1]
for i in range(2, n + 1):
table.append(table[i - 1] + table[i - 2])
return table[n]
print(fib_table(10))
Prints
55
It works whenever a problem breaks into smaller problems that overlap: the fewest coins to make an amount, the cheapest route across a grid, how similar two words are (spell checkers), or which items fit best in a bag of limited size.
Practice
Why is the plain recursive fib(n) so slow?
More info
Quick reference
Overlapping subproblems
The same smaller problems come up again and again.
Memoization
Top-down: cache each answer the first time.
Tabulation
Bottom-up: fill a table from the smallest case.
@cache
Python's functools decorator that memoizes a function.