Run it
The call count for plain recursion, which is the argument for everything that follows.
The three fixes, agreeing, with the space each one uses.
And the reason bottom-up is not merely a preference.
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 table — bottom-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.