Master

Top-down versus bottom-up

The same recurrence can run top-down with recursion plus a cache, or bottom-up by filling a table from the small cases up. Both compute the same values at the same cost.

What you will see

5

About 3 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Solve one recurrence two ways.

1

Coins for 6 top-down.

from functools import lru_cache @lru_cache(maxsize=None) def coins(n): if n == 0: return 1 if n < 0: return 0 return coins(n - 1) + coins(n - 2) print(coins(6))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
21
2

Coins for 6 bottom-up.

def coins(n): dp = [0] * (n + 1) dp[0] = 1 for i in range(1, n + 1): dp[i] = dp[i - 1] + (dp[i - 2] if i >= 2 else 0) return dp[n] print(coins(6))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
21
3

Fib 50 rolling.

def fib(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return a print(fib(50))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
12586269025
4

Coins for 4 bottom-up.

def coins(n): dp = [0] * (n + 1) dp[0] = 1 for i in range(1, n + 1): dp[i] = dp[i - 1] + (dp[i - 2] if i >= 2 else 0) return dp[n] print(coins(4))

Before you run it, say out loud what it will print. Then open the answer.

What it prints
5

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A recurrence can run top-down with recursion plus a cache or bottom-up by filling a table from small cases. Both cost the same and agree.
1 write the recurrence
↓
2 top-down cache or bottom-up table
↓
3 fill in dependency order
↓
4 same values
Line by line

Read the first example one line at a time. A line that prints shows what appears on screen; the rest set things up quietly.

from functools import lru_cache sets nothing on screen
@lru_cache(maxsize=None) sets nothing on screen
def coins(n): sets nothing on screen
if n == 0: sets nothing on screen
return 1 sets nothing on screen
if n < 0: sets nothing on screen
return 0 sets nothing on screen
return coins(n - 1) + coins(n - 2) sets nothing on screen
print(coins(6)) 21
Why this works
Same recurrence, two orders

Top-down recurses from the goal down to the base with a cache. Bottom-up starts at the base and fills up. Both are the same set of equations.

Bottom-up is stack-safe

When n is large, bottom-up avoids the recursion limit entirely. That is the reason competitive code often prefers it.

Examples

Top-down with cache

from functools import lru_cache
@lru_cache(maxsize=None)
def coins(n):
    if n == 0:
        return 1
    if n < 0:
        return 0
    return coins(n - 1) + coins(n - 2)
print(coins(6))

Recursion plus a cache is top-down. It computes only the states it reaches.

Bottom-up table

def coins(n):
    dp = [0] * (n + 1)
    dp[0] = 1
    for i in range(1, n + 1):
        dp[i] = dp[i - 1] + (dp[i - 2] if i >= 2 else 0)
    return dp[n]
print(coins(6))

Fill the table from zero upward. No recursion and no cache, just a loop.

Same answer both ways

from functools import lru_cache
@lru_cache(maxsize=None)
def f(n):
    if n < 2:
        return n
    return f(n - 1) + f(n - 2)
def g(n):
    dp = [0] * (max(n, 1) + 1)
    dp[0], dp[1] = 0, 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]
print(f(7) == g(7))

Top-down and bottom-up agree. The recurrence is the same, the order differs.

Bottom-up avoids depth

def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a
print(fib(50))

A rolling table uses constant memory and no stack. Deep n is safe.

Top-down reaches less

from functools import lru_cache
@lru_cache(maxsize=None)
def f(n):
    if n < 2:
        return n
    return f(n - 1) + f(n - 2)
print(f(10))

Top-down computes only needed states. Bottom-up fills every index up to n.

Order of filling

def coins(n):
    dp = [0] * (n + 1)
    dp[0] = 1
    for i in range(1, n + 1):
        dp[i] = dp[i - 1] + (dp[i - 2] if i >= 2 else 0)
    return dp[n]
print(coins(4))

Each cell needs smaller cells, so fill left to right. The dependency sets the order.

Space can shrink

def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a
print(fib(10))

Only the last two values are read, so keep just those. The table shrinks to two cells.

Pick by need

from functools import lru_cache
@lru_cache(maxsize=None)
def f(n):
    if n < 2:
        return n
    return f(n - 1) + f(n - 2)
print(f(3))

Top-down reads like the recurrence and skips unused states. Bottom-up is faster when you need all of them.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Tail recursion and loops
Previous Lesson
Tail recursion and loops
Next Lesson
When recursion is too deep
When recursion is too deep →