Climbing stairs: the DP that is Fibonacci

ways(n) = ways(n-1) + ways(n-2), because the last move was either one step or two. That is Fibonacci. Written as plain recursion it is exponential; the same recurrence computed once per value is O(n), and since only the last two values are ever needed it is O(1) space.

Overview

The question, and what it is testing

ways(n) = ways(n-1) + ways(n-2), because the last move was either one step or two. That is Fibonacci. Written as plain recursion it is exponential; the same recurrence computed once per value is O(n), and since only the last two values are ever needed it is O(1) space.

Dynamic programmingCoding problemEasy

Step through it

What to watch

  • The call count roughly doubles per extra step — that is the overlap, measured.
  • Filling forwards computes each value exactly once.
  • Only the last two entries are ever read, which is why the table can go.

Say this out loud

"The last move was either a single step from n-1 or a double from n-2, so ways(n) = ways(n-1) + ways(n-2) with ways(1) = 1 and ways(2) = 2. It is Fibonacci. Naive recursion is exponential because the subproblems overlap, so I either memoise or fill a table forwards - and since only the last two entries are read, two variables are enough. O(n) time, O(1) space."

Climbing stairs: the DP that is Fibonacci

You can climb 1 or 2 steps at a time. How many ways to reach step n?

Run it

The call count for plain recursion, which is the argument for everything that follows.

1Python
Output

The three fixes, agreeing, with the space each one uses.

2Python
Output

And the reason bottom-up is not merely a preference.

3Python
Output

Where the recurrence comes from

Work backwards from the destination rather than forwards from the start. To be standing on step n, the previous step was either n−1 (and you took one step) or n−2 (and you took two). Those two sets of routes are disjoint and together they are all of them, so the counts add.

That is the whole derivation, and it is worth saying in exactly that form: every route ends with one of two moves, so partition by the last move. The same sentence derives the recurrence for coin change, for house robber, and for most one-dimensional DP.

The base cases are where people slip. ways(1) = 1 and ways(2) = 2 — and if you define ways(0) = 1 (one way to stand still) the recurrence works from n = 2 and the sequence lines up with standard Fibonacci.

Why plain recursion is exponential

ways(5) calls ways(4) and ways(3); ways(4) calls ways(3) again. The recursion tree has two branches at almost every node and a depth of n, so the number of calls grows like the golden ratio to the n — and the editor below prints it: 109 calls at n = 10, 13,529 at n = 20, over 150,000 at n = 25.

This is overlapping subproblems, and it is one of the two conditions that make a problem a DP problem. The other is optimal substructure: the answer for n is built from the answers for smaller n, unchanged by how you got there. Naming both is a good way to show you know why the method applies rather than pattern-matching to it.

Four versions, in the order to present them

Recursion — states the recurrence clearly and is unusable. Write it, say it is exponential, move on.

Memoised recursion@functools.lru_cache on the same function. One line, O(n) time, O(n) space, and it keeps the recursive shape that made the recurrence obvious. This is top-down.

A forwards tablebottom-up, no recursion, no stack depth limit. Same complexity, and it makes the next step visible.

Two variables — because the table is only ever read two entries back. O(1) space, and the version to end on. Offering all four in that order, briefly, is worth more than jumping straight to the last one: the interviewer wants the reasoning, and the reasoning is the ladder.

The follow-ups

"What if you can climb 1, 2 or 3 steps?" The recurrence gains a term and the rolling window becomes three variables. The general version — any set of allowed step sizes — is coin change counting combinations, which is the next question.

"Can it be faster than O(n)?" Yes, and this is the answer that surprises people: Fibonacci has a closed form, and matrix exponentiation computes it in O(log n) multiplications. Worth naming; not worth writing unless asked, because the numbers get big enough that the multiplications stop being O(1).

What to say out loud

The last move was either a single step from n-1 or a double from n-2, so ways(n) = ways(n-1) + ways(n-2) with ways(1) = 1 and ways(2) = 2. It is Fibonacci. Naive recursion is exponential because the subproblems overlap, so I either memoise or fill a table forwards - and since only the last two entries are read, two variables are enough. O(n) time, O(1) space.

Edge cases to raise

Volunteering these is most of what separates a correct answer from a good one.

n = 0 and n = 1. Decide whether standing still counts as one way. Defining ways(0) = 1 makes the recurrence work from n = 2 with no special cases.

Large n. The numbers get big - rolling(4000) has 836 digits. Python handles it; a language with fixed-width integers overflows silently.

Recursion depth. The memoised version dies around n = 1000 by default. That is the practical argument for bottom-up, not a stylistic one.

The follow-ups interviewers ask

"What if you can climb 1, 2 or 3 steps?" One more term in the recurrence and a three-variable window. The general version - an arbitrary set of step sizes - is coin change counting combinations.

"Can it be faster than O(n)?" Yes. Fibonacci has a closed form, and matrix exponentiation gets it in O(log n) multiplications. Worth naming; not worth writing, because at that size the multiplications stop being O(1).

"What if some steps are broken and cannot be used?" The recurrence gains a guard: ways(n) = 0 for a broken step, and the rest is unchanged. It is a good check on whether you understand the recurrence or memorised the Fibonacci answer.

Common wrong answers

"It is 2 to the n, one choice per step." That counts sequences of moves without requiring them to land on n. The constraint is the total, which is what makes it Fibonacci rather than exponential.

"Memoisation and dynamic programming are different things." They are the same recurrence computed top-down or bottom-up. Presenting them as alternatives rather than as two directions is a common muddle.

"Just use lru_cache and stop." Correct and it keeps O(n) space plus a recursion depth that fails around n = 1000. The editor shows the RecursionError.

Recap in one screen

  • The call count roughly doubles per extra step - that is the overlap, measured.
  • Filling forwards computes each value exactly once.
  • Only the last two entries are ever read, which is why the table can go.
  • Worth trying: Raise the naive call to naive(30) and watch the count. It is about 1.6 times worse per step, which is the golden ratio showing up in the runtime.
  • Worth trying: Add a third step size - a, b, c = b, c, a + b + c - and check ways(1..8) against counting by hand for small n.

How the code works

The call count for plain recursion at three sizes, then the three fixes agreeing on the same answer - with the space each one uses.

How the code works

  1. calls += 1The counter is the argument. "Exponential" is a claim; 150,049 calls for n = 25 is a measurement, and it is what justifies everything that follows.
  2. @functools.lru_cache(maxsize=None)Top-down memoisation in one line, keeping the recursive shape. The cache size printed afterwards is n — one entry per distinct subproblem, which is the definition of the fix.
  3. a, b = b, a + bThe whole table collapsed into two names, because nothing ever reads further back than two. This is the version to end on.
  4. memo(4_000) -> RecursionErrorWhy bottom-up is not just a stylistic preference: the recursive version has a depth limit and the iterative one does not.

Change one thing

  • Raise the naive call to naive(30) and watch the count. It is about 1.6 times worse per step, which is the golden ratio showing up in the runtime.
  • Add a third step size — a, b, c = b, c, a + b + c — and check ways(1..8) against counting by hand for small n.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Where does ways(n) = ways(n-1) + ways(n-2) come from?

  2. Why is plain recursion exponential here?

  3. Why can the table be replaced by two variables?

  4. What does the sequence turn out to be?

Cheat sheet

Climbing stairs: the DP that is Fibonacci

ways(n) = ways(n-1) + ways(n-2), because the last move was either one step or two. That is Fibonacci. Written as plain recursion it is exponential; the same recurrence computed once per value is O(n), and since only the last two values are ever needed it is O(1) space.

INTERVIEW · vizlearn.in/interview/climbing-stairs.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.