“Know how to solve every problem that has been solved.”“What I cannot create, I do not understand.”— Richard Feynman
Fibonacci Sequence
Algorithms
The Fibonacci sequence is a series of numbers where each number is the sum of the two preceding ones, typically starting with 0 and 1.
Definition
The Fibonacci sequence is defined as:
F0=0,F1=1Fn=Fn−1+Fn−2for n≥2
Dynamic Programming Approach
Using dynamic programming, we can compute Fibonacci numbers efficiently in O(n) time with O(1) space using an optimized approach.
Implementation
def fibonacci(n, optimized=True): if n <= 0: return 0 elif n == 1: return 1 if optimized: # Space-Optimized Approach a, b = 0, 1 for _ in range(2, n + 1): a, b = b, a + b return b else: # Full List Approach fib = [0] * (n + 1) fib[1] = 1 for i in range(2, n + 1): fib[i] = fib[i - 1] + fib[i - 2] return fib[n]# Example usage:print(fibonacci(10)) # Output: 55 (optimized by default)print(fibonacci(10, optimized=False)) # Output: 55 (using full list)
Try First
Each prompt asks a checkable question about the working code or math
above — predict an output, derive a sign, state an invariant, find a
bug. Commit to an answer before clicking "reveal." That commitment is
the whole point: if your answer matched, you understand the piece you
were looking at; if it didn't, that's the part worth re-reading.
invariant
At the top of iteration k of the optimized loop (so just
before the assignment runs), what do a and b
hold? Phrase your answer in terms of F. Be precise about
indices — the loop starts at k = 2.
answer
At the top of iteration k (for k = 2, 3, ..., n):
a = F(k-2) and b = F(k-1). After the
assignment a, b = b, a + b, we have a = F(k-1)
and b = F(k). So when the loop terminates after running
with k = n, b = F(n) as required.
The variable names hide this. a and b are
not "two scratch registers" — they're the two most recent values of
the sequence at all times, with a specific index lag. Renaming them
to F_prev_prev and F_prev makes the code
slower to read but harder to misunderstand. Worth the trade if
you're authoring; not worth it if you're golfing.
predict
Without running the code, predict fibonacci(20). Use
Binet's closed form: F(n)=5φn−ψn
where φ=(1+5)/2≈1.618 and
ψ=(1−5)/2≈−0.618. For
n = 20, the ψn term is tiny — about
5·10^-5 — so rounding φ20/5
should give the answer. Now: at what n does floating-point
error start producing the wrong integer?
Double precision carries about 15–16 decimal digits. The Fibonacci
sequence grows like φn, hitting 16 digits around
F(75) ≈ 2·10^15. By F(80) ≈ 2.3·10^16
you're already at the precision boundary; Binet starts disagreeing
with the integer recurrence in the last digit. By F(100)
the closed form is just wrong.
The CS lesson: an exact integer recurrence in O(n) time beats an
"O(1) closed form" if the closed form requires more precision than
your floats provide. Same shape of trap shows up everywhere from
chaotic ODEs to GPU-accelerated matrix powers.
why does this work
Compute F(n) mod 7 for n = 0, 1, 2, ..., 20
and write out the sequence. You'll find it repeats with a period (the
Pisano period for m = 7). Why is the existence
of some period — for any modulus m — guaranteed?
The argument should come from the structure of the recurrence, and a
counting bound on the possible states it can be in.
answer
For m = 7 the sequence mod 7 is
0, 1, 1, 2, 3, 5, 1, 6, 0, 6, 6, 5, 4, 2, 6, 1, 0, 1, 1, 2, 3, ...
and the period is 16. Notice the pair (0, 1) at indices
0 and 16 — the moment you see the starting pair again, the entire
future of the sequence repeats.
Why must some period exist? The state of the recurrence at any
point is fully determined by the pair (F(n) mod m, F(n+1) mod m).
There are only m² such pairs. So after at most m²
steps, some pair must repeat by the pigeonhole principle. Once a
pair repeats, the future of the sequence is determined by that
pair — so the sequence is periodic from that point on. The recurrence
is also reversible (you can recover F(n-1) from
F(n) and F(n+1)), which forces the
repetition to occur from the very beginning, not just from some
later point.
This is why F(10^18) mod 1000 is a one-line problem:
compute the period (cheap), reduce 10^18 mod that
period, run the linear loop on the small remainder.
find the bug
I changed a, b = b, a + b to a, b = b, max(a, b).
The code still runs and still returns a number. What does it now
compute, and for what input would you actually want this algorithm?
answer
After the change, b is updated to max(a, b)
each iteration. Since the sequence is monotone non-decreasing
(b only ever grows), max(a, b) = b every
time — the value of b never changes after iteration 2.
It just gets re-assigned to itself.
So the broken Fibonacci returns b = 1 regardless of
n (for n ≥ 1). Not a useful algorithm for
anything in particular — unlike the House Robber probe with the
same flavor of mutation, where the broken code became a running max.
Worth knowing: not every operator swap reveals a hidden algorithm.
Sometimes a swap just produces a constant function. The diagnostic
value of the exercise is recognizing which case you're in.
equivalence
Show that (1110)n=(F(n+1)F(n)F(n)F(n−1)).
What does this matrix identity buy you computationally over the linear
loop above?
hint
Prove by induction on n. The base case is n = 1;
the inductive step uses one matrix multiplication and the Fibonacci
recurrence.
answer
Once you have the identity, you can compute the n-th
Fibonacci number by raising the matrix to the n-th
power. Matrix power via repeated squaring is O(log n) multiplications
— versus O(n) for the linear loop. Each multiplication is on
2×2 integers, so the constant factor is friendly. For
n = 10^18 mod m (combined with the Pisano-period
trick from the previous probe, when you DON'T want to find the
period), this is the standard approach. It generalizes: any linear
recurrence of bounded order can be computed in O(k³ log n) via the
same trick, where k is the order. The technique is
called "Kitamasa" or "linear recurrence by matrix exponentiation"
depending on which corner of the literature you read.
Hack This
The code above works. Don't reinvent it — pull it into an editor, run it, and
try the modifications below. Each one is small. Each one will change the behavior
in a specific way; the question is which way.
trivial
The Fibonacci recurrence runs both directions: rearranging
F(n)=F(n−1)+F(n−2) gives F(n−2)=F(n)−F(n−1).
So F(-1) = 1, F(-2) = -1, F(-3) = 2,
F(-4) = -3, F(-5) = 5 — the magnitudes match
the positive side, but the signs alternate. Modify the optimized
function to accept negative n and return the right value.
Then: why does the sign alternate specifically in that
pattern? Predict the sign of F(-100) without computing
it.
hint
The alternation pattern is dictated by the closed form. With
ψ=(1−5)/2 being negative, ψn
flips sign with every step of n — and on the negative
side, the ψn term dominates rather than vanishes.
trivial
Change the initialization to a, b = 2, 1. The recurrence
is unchanged but the sequence becomes the Lucas numbers:
2, 1, 3, 4, 7, 11, 18, 29, 47, .... Verify by running.
Then: what's the closed form? It uses the same φ
and ψ as Binet — only the coefficients change. Find
them.
hint
Every solution to the recurrence an=an−1+an−2
is a linear combination of φn and ψn.
The initial conditions pin down which linear combination. For
Lucas: L(n)=φn+ψn. No 5,
no fraction. Cleaner than Binet, in some moods.
small
Generalize the rolling pair to compute
an=p⋅an−1+q⋅an−2 for arbitrary
integer p, q. With p = q = 1 you get
Fibonacci. With p = 2, q = 1 you get Pell numbers
(0, 1, 2, 5, 12, 29, ...). With p = 1, q = 2
you get the Jacobsthal numbers (0, 1, 1, 3, 5, 11, 21, ...).
What's the growth rate of the generalized sequence as a function of
p, q? When is the growth rate even real?
hint
The growth rate is the dominant root of the characteristic equation
x2=px+q. Real roots when p2+4q≥0;
complex conjugate pair otherwise, in which case the sequence
oscillates with bounded or growing amplitude depending on the
modulus of the root.
small
Define T(n) = T(n-1) + T(n-2) + T(n-3) with
T(0) = 0, T(1) = 0, T(2) = 1. Modify the optimized code
to compute it. You'll need more state — the rolling pair becomes a
rolling triple. The growth rate is the tribonacci constant
η≈1.839 (replacing the golden ratio).
Predict T(20) using
T(n)≈C⋅ηn (the constant
C≈0.18), then verify.
hint
Three variables: a, b, c = 0, 0, 1. Each step:
a, b, c = b, c, a + b + c. Return c
(with appropriate handling for small n). For
n = 20, the prediction 0.18⋅1.83920
gives roughly 35,890 — close to the true value 35,890. (Yes, exactly.
The leading coefficient and constant are both close enough that
rounding lands on the integer.)
medium
Implement fib_log(n) using the matrix identity from the
Under-the-Hood probe: Mn where
M=(1110),
computed via repeated squaring (use Python's ** for ints
but write the 2×2 matrix power yourself). Verify it against the linear
loop for n = 0..50. Then run it with n = 10^18 mod (10^9 + 7)
— a problem that would take longer than the heat death of the
universe with the linear loop, and a few microseconds here. Compare
its asymptotic cost to the Pisano-period approach. Which one
wins, and for what kind of input?
hint
Matrix exponentiation is O(log n) regardless of modulus. The
Pisano-period approach is O(period(m)) to find the period, then
O(n mod period(m)) to compute. For small m the period
is tiny and Pisano dominates. For large m (especially
prime m), the period can approach m and
the matrix approach wins.
medium
Compute fibonacci(10**8) % 1000 using the existing
optimized loop. Time it. Now modify the loop to take the modulus
inside the loop body — a, b = b, (a + b) % 1000
— and time that. Why is the second version dramatically faster, even
though both do the same number of iterations? What's the running
magnitude of b in each case at iteration 10^7?
hint
Python ints are arbitrary precision. By iteration 10^7
the value of b in the unmodded version has about
107⋅log10(φ)≈2.1⋅106 digits.
Each addition of two such numbers is O(digits) — turning the loop
from O(n) primitive operations into O(n²) bit operations. The
modded version keeps every value below 1000, so each addition is
actually constant-time.