Fibonacci Numbers
Compute F(n) = F(n-1) + F(n-2) in linear time by reusing the two previous values instead of recomputing them.
Overview
The Fibonacci sequence is 0, 1, 1, 2, 3, 5, 8, 13, …: every term is the sum of the two before it. It is the smallest problem that shows the whole DP toolkit — a recurrence, overlapping subproblems, a memoized top-down solver, a tabulated bottom-up solver, and a space optimization down to two variables.
The naive recursive definition fib(n) = fib(n-1) + fib(n-2) runs in O(φⁿ) (φ ≈ 1.618) because it recomputes the same subproblems exponentially many times. Recording each answer once collapses that to O(n). With matrix exponentiation (see Fast Exponentiation) it can even be O(log n), but the linear version is what interviews expect.
Intuition
A mental model before the formal terms.
Draw the call tree for fib(5): it calls fib(4) and fib(3); fib(4) calls fib(3) and fib(2) again. fib(3) is computed twice, fib(2) three times, fib(1) five times. For fib(50) the duplication is astronomical — about 2×10¹⁰ calls to produce a number you could write on a sticky note.
Now imagine writing each answer on the sticky note the first time you compute it. The second request for fib(3) is a lookup, not a recomputation. The tree collapses into a single chain fib(1) → fib(2) → … → fib(n), and that chain is exactly what the bottom-up loop walks directly.
How it works
- State:
dp[i]= the i-th Fibonacci number. One integer index fully describes a subproblem. - Transition:
dp[i] = dp[i-1] + dp[i-2]fori ≥ 2. - Base case:
dp[0] = 0,dp[1] = 1. - Iteration order: increasing
ifrom 2 to n, because each state depends only on smaller indices. Top-down memoization gets the same order implicitly through recursion. - Answer location:
dp[n]. - Space optimization: only the previous two values are ever read, so keep two variables
a, band slide them:a, b = b, a + b. Space drops fromO(n)toO(1).
Why it works
Optimal substructure here is literal: F(n) is defined in terms of F(n-1) and F(n-2), and those values do not depend on how or why they were requested. So a value computed once for one caller is correct for every caller.
Overlapping subproblems: there are only n + 1 distinct subproblems (F(0) … F(n)) but the naive tree makes exponentially many calls. Caching guarantees each distinct state is solved exactly once, and each takes O(1) work, so the total is O(n).
Bottom-up correctness is by induction on i: if dp[i-1] and dp[i-2] are correct, then dp[i] computed from them is correct, and the bases are correct by definition.
Recognition
How to tell a problem wants this.
- A quantity is defined by "the previous one plus the one before that" — Fibonacci, tribonacci, tiling a
2×nboard with dominoes, counting binary strings without consecutive ones. - A naive recursive solution is obviously exponential and you can see the same call repeated in the tree.
nup to10^5–10^7rules out recursion depth in most languages and points at the iterative two-variable form.
Interactive visualization
Play, step, change the input. ← → and space work too.
| n | fib(n) |
|---|
1fib(n):2 if n <= 1: return n3 if n in memo: return memo[n]4 memo[n] = fib(n-1) + fib(n-2)5 return memo[n]Pseudocode
1if n < 2: return n2a = 0, b = 13for i in 2..n:4 a, b = b, a + b5return bImplementations
1# Tabulated: dp[i] = F(i). Python ints never overflow.2def fib(n: int) -> int:31 · State table4 if n < 2:5 return n6 dp = [0] * (n + 1)72 · Base cases8 dp[0] = 09 dp[1] = 1103 · Transition11 for i in range(2, n + 1):12 dp[i] = dp[i - 1] + dp[i - 2]134 · Answer14 return dp[n]15 16 17# Rolling variables, O(1) space18def fib_rolling(n: int) -> int:19 a, b = 0, 120 for _ in range(n):21 a, b = b, a + b22 return a[0] * (n + 1)builds the table; multiplying a list of an immutable int is safe (no aliasing, unlike lists of lists).- Base cases are set at 0 and 1 after the
n < 2guard sodp[1]exists. range(2, n + 1)is inclusive ofn— the+ 1is the standard off-by-one to remember.fib_rollinguses tuple assignmenta, b = b, a + b, which evaluates the right side fully before assigning.
Rolling version O(1). Python ints are arbitrary precision, so F(1000) is exact; big-int addition becomes O(digits) for very large n.
functools.lru_cachegives memoization for free but recursion depth is capped at ~1000 by default (sys.setrecursionlimithelps only up to the C stack).- Tuple swap
a, b = b, a + bis the idiomatic rolling update — no temp variable needed. - Arbitrary-precision ints mean there is no overflow to worry about, only time proportional to digit count.
- Calling the memoized recursive version with n = 10**5 —
RecursionError. Use the loop. - Writing
for i in range(2, n)and returningdp[n], which is still 0. - Placing
@lru_cacheon a method that takes an unhashable argument (like a list) — TypeError at call time.
- Overflow: C++
long longoverflows at F(93) (undefined behaviour); JS/TSnumbersilently loses precision after F(78); Python ints are exact forever. - Exact large values: use
BigIntin JS/TS,__int128or a big-int library in C++; Python needs nothing. - Recursion limits for the memoized version: Python ~1000 frames by default, JS/TS ~10^4, C++ typically ~10^5 — prefer the loop for large n in all four.
- Tuple assignment
a, b = b, a + bexists in Python; JS/TS destructuring allocates an array; C++ needs a temporary (orstd::tie).
Complexity
Memoized version uses O(n) space for the cache and the recursion stack. Naive recursion is O(φⁿ). Matrix exponentiation gives O(log n).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Any linear recurrence with a constant number of previous terms — it is the template for Climbing Stairs, tiling, and "count ways" problems.
- As the first sanity check when learning Memoization (Top-Down DP) vs Tabulation (Bottom-Up DP): both are a few lines and the speedup is dramatic.
- When
nis around10^18and you needF(n) mod m— use matrix exponentiation or fast doubling,O(log n). - When exact values are needed beyond
F(92)in 64-bit languages — the result overflows; use big integers or modular arithmetic.
Alternatives
Common mistakes
- Writing the memoized version without the cache and calling it "DP" — it is still exponential.
- Off-by-one on which of the two rolling variables holds the answer after the loop.
- Recursion depth:
fib_memo(10**5)in Python overflows the default stack; use the iterative form for largen. - Overflow in Java/C++/Go:
F(93)exceeds a signed 64-bit integer.
Interview patterns
- Climbing stairs (1 or 2 steps) is Fibonacci shifted by one index.
- Tribonacci and "ways to tile a 2×n board" — same structure with a different number of previous terms.
- "Compute F(n) mod 10^9+7 for n up to 10^18" — matrix exponentiation on
[[1,1],[1,0]].
- Coin ChangeIntermediate