Climbing Stairs
Introduction
A staircase of steps. At each move you can hop one step or two. The question is plain: in how many distinct ways can you reach the top? Most candidates write a recursive solution in fifteen seconds and stop. That fifteen-second solution is exponential, and the gap between “obvious” and “good enough” is exactly why this problem keeps appearing.
This is the rite-of-passage problem for dynamic programming. Four approaches walk a careful arc — exponential recursion, memoized recursion, bottom-up sweep, and a matrix-exponentiation reframing — and each one teaches a distinct DP move that reappears across half a dozen later problems. The lesson isn’t the algorithm. It’s the discipline of asking, “my recursion is correct — what is it doing twice?”, and recognizing the answer.
The interview test isn’t writing the recurrence. It’s noticing what the recurrence is doing twice — and trading work for memory.
If you’re early in DP prep, walk all four approaches in order — the lessons compound. If you can write the bottom-up answer in your sleep, jump to the matrix-exponentiation section: it’s the unfamiliar one and the one most worth knowing.
What the interviewer is actually testing
- Recurrence recognition. Can you state, in one sentence, why the answer for stairs equals the answer for plus the answer for ? Candidates who can’t articulate that miss the entire problem.
- Awareness of overlapping subproblems. The naive recursion is correct; recognizing that it’s exponential because subproblems repeat is the DP pivot.
- Space discipline. The bottom-up array is ; collapsing to two variables is . Spotting that compression is a senior signal.
- Knowing the next level. Matrix exponentiation isn’t required for this problem in most interviews — but mentioning it as the option signals you’ve been deeper into algorithmics than the median candidate.
Problem statement
You’re climbing a staircase with steps. At each move you can hop either one step or two. Return the number of distinct ordered sequences of hops that bring you exactly to the top.
n = 2 2 n = 4 5 n = 7 21 Clarifying questions — and why each one matters
Clarification questions
- Q What's the upper bound on n?
- A The most important clarification. For n ≤ 45, a 32-bit int holds the answer. By n = 100 the count exceeds 2⁶³ and you need big integers. By n = 10⁹ the linear approaches are too slow and matrix exponentiation becomes the only viable choice.
- Q Are the hops just 1 and 2, or can the step set be arbitrary?
- A Default is {1, 2}. If the interviewer generalizes to an arbitrary step set S, the recurrence becomes
f(n) = sum over s in S of f(n − s)— same DP shape, just a wider summation. The matrix-exponentiation trick still applies but the matrix gets bigger. - Q Does the order of hops matter?
- A Yes. The problem asks for distinct *ordered* sequences.
1 + 2and2 + 1count as two different ways. If the interviewer says order doesn't matter, the problem becomes a partition-counting problem and the recurrence changes. - Q Does n = 0 count as one way (the empty climb) or zero ways?
- A Convention varies. The cleanest mathematical convention is f(0) = 1 — the empty sequence is a valid 'way' to climb zero stairs. Without that, the recurrence has an awkward special case. Confirm with the interviewer; the LeetCode statement assumes n ≥ 1, so n = 0 doesn't usually arise.
- Q Are negative n or non-integer n possible?
- A No, by problem definition. If they were, the recurrence would need bounds checks; the standard formulation guarantees a positive integer.
A mental framework — the recurrence
Think of the very last hop. To stand on stair , the previous position was either stair (and you took a 1-step) or stair (and you took a 2-step). Those are the only two cases — they don’t overlap, and together they cover everything.
So the number of ways to reach stair is the number of ways to reach stair plus the number of ways to reach stair :
with base cases . That’s the entire algorithmic content. Everything in the rest of the chapter is how to evaluate this recurrence efficiently.
If the recurrence looks familiar, it should: is the Fibonacci sequence, shifted by one. The chapter’s four approaches are the standard four ways to evaluate Fibonacci, and the lessons transfer to every recurrence with the same “constant-state” shape.
Discussion
Naive recursion — the recurrence, written verbatim
Translate the recurrence directly into recursive calls. Correct, exponential, your starting line.
The interview narration
Say this out loud:
“The recurrence is with . The most direct translation is two recursive calls. That gives the right answer but the call tree branches at every level — time. I’ll write it as a baseline then look for what’s expensive.”
How it works, step by step
The function takes , and:
- Base cases: if , return — there’s exactly one way to “climb” 0 or 1 stairs.
- Recursive case: otherwise, return
climbStairs(n - 1) + climbStairs(n - 2).
That’s it. The code is a transcription of the recurrence.
The code
int climbStairs(int n) { if (n <= 1) return 1; return climbStairs(n - 1) + climbStairs(n - 2);}Three lines. Correct on every input. Will hang on n = 50.
Why it’s exponential — look at the tree
The recursion tree branches into two children at every node. The leaves are calls with , each returning 1. The total number of leaves equals the answer , which is the -th Fibonacci number — a quantity that grows as where .
That’s already enough to make slow ( calls) and unbearable ( calls). The problem isn’t the recursion — it’s that the same subproblems are computed repeatedly. In the figure above, is computed three separate times for . For larger , the redundancy explodes.
What it gets right
- Correctness. No edge cases to worry about; the base case handles and uniformly.
- It exposes the optimization. The recursion tree is the visual proof that overlapping subproblems exist. Once you see the tree, memoization is obvious.
Why it’s rejected
For this would take hours. Production code never ships exponential when polynomial exists. But the interview value of writing it is real: it’s how you demonstrate you’ve read the problem correctly before optimizing.
Memoized recursion — cache the repeats
Same recursion, with a memo table to ensure each subproblem is solved exactly once. Linear time, linear space.
The insight
Look at the recursion tree above. The same f(k) value appears multiple times — sometimes dozens of times for moderately large . Each repetition recomputes a result we already produced. Caching the answer the first time we compute it, and returning the cached value thereafter, drops the algorithm from exponential to linear.
The cache: an array memo[0..n] holding either the computed answer or a sentinel () for “not yet computed.”
How it works, step by step
The recursion is unchanged except for two extra lines:
- Before computing, check the cache. If
memo[n] != -1, return it immediately. - After computing, store the result in
memo[n]before returning.
The cache turns the recursion tree into a recursion DAG: each subproblem has a single computation, and subsequent calls just retrieve the answer. Total work: subproblems × work each = .
The code
#include <vector>
namespace {int solve(int n, std::vector<int>& memo) { if (n <= 1) return 1; if (memo[n] != -1) return memo[n]; memo[n] = solve(n - 1, memo) + solve(n - 2, memo); return memo[n];}} // namespace
int climbStairs(int n) { std::vector<int> memo(n + 1, -1); return solve(n, memo);}The recursive structure is preserved; the caching is the only addition. The anonymous-namespace `solve` helper keeps the public API clean while sharing the memo across recursive calls.
A few details worth naming:
- Sentinel
-1works because every answer is positive. If 0 or negative were valid, switch tostd::optional<int>. - Stack depth still goes deep on the first call, so very large can overflow the call stack. Bottom-up (Approach 03) avoids it.
Complexity
- Time: . Each subproblem is computed exactly once.
- Space: for the memo table plus recursion depth.
When memoized recursion beats bottom-up
- When the recurrence has irregular dependencies. Climbing Stairs has a clean linear dependency, so bottom-up wins. But for problems where the set of needed subproblems is sparse (only a fraction of all possible are ever requested), top-down with memoization avoids computing the unused ones.
- When the recursion mirrors the problem statement intuitively. Some problems read more naturally as “given , recurse on smaller inputs” than as “build up from base cases.” Reach for the form that’s easier to argue correct.
Bottom-up — two variables, one pass Recommended
Iterate from i = 2 up to n, computing each f(i) from the previous two values. The full memo array collapses to two integers.
The insight, stated cleanly
Watch the memoized recursion compute its values. By the time we know , we’ve stored every for . But we only ever use the two most recent values — once is computed, and earlier are never read again.
That observation has two consequences. First, we can fill the array in order rather than recursively (which avoids the recursion overhead and the stack depth). Second, the array itself is unnecessary: at any moment, only the last two values matter. The whole memo collapses to two integers that we slide forward each iteration.
Initialize an array of size n+1 = 6.
How it works, step by step
Maintain two running values:
prev2— the answer for two stairs ago. Initialized to .prev1— the answer for one stair ago. Initialized to .
For from up to :
- Compute
curr = prev1 + prev2. That’s . - Slide:
prev2 = prev1,prev1 = curr. Nowprev1is the latest value,prev2is the one before that.
After the loop, prev1 holds .
The code
int climbStairs(int n) { if (n <= 1) return 1; int prev2 = 1; // ways to climb 0 stairs int prev1 = 1; // ways to climb 1 stair for (int i = 2; i <= n; ++i) { const int curr = prev1 + prev2; prev2 = prev1; prev1 = curr; } return prev1;}Six lines of body. Two integers of state. The whole DP table fits in a couple of registers.
Why this is the production answer
- Constant space. A signed 32-bit
intholds for ( overflowsINT_MAX). A signed 64-bitlong longholds it for . Beyond that you need big integers, but the algorithm is the same. - No recursion. No stack-overflow risk for large .
- One pass, simple loop. The branch predictor and the cache both love this code.
- Easy to verify. Trace it on in your head: 1, 1, 2, 3, 5, 8. Done.
Edge cases
- : the loop never runs;
prev1 = 1is returned. Matches the convention . - : the loop never runs;
prev1 = 1is returned. Correct. - Large near the int limit: already overflows a signed 32-bit integer (it’s just above ). Switch to
long longif the spec hints at much past 45; this is a one-character code change and an easy interview win.
Complexity
- Time: . Single pass with constant-time work per iteration.
- Space: . Two integers.
Matrix exponentiation — O(log n) for the senior signal
Encode the Fibonacci recurrence as a 2×2 matrix multiplication. Repeated squaring evaluates M^n in O(log n) multiplications, giving a logarithmic-time answer.
The insight
The recurrence is a linear transformation on the pair . Linear transformations compose via matrix multiplication, and matrix multiplication has a fast-power algorithm (the same trick that lets you compute in via repeated squaring).
The transformation matrix is:
It acts on the state vector to produce — one Fibonacci step per matrix multiplication. A direct algebraic identity falls out: every entry of is itself a value of :
So — our answer — sits at position of . Compute via repeated squaring and read off the top-left entry. That’s the whole algorithm.
How it works, step by step
The whole algorithm is repeated squaring of the matrix:
- Initialize the running result to the identity matrix , and let
base = M. - Walk the bits of , low-to-high. While :
- If the lowest bit of is set, multiply the running result by
base. - Square
basein place:base = base · base. - Right-shift by one.
- If the lowest bit of is set, multiply the running result by
- Return the top-left entry of the final result.
The total number of multiplications is at most (one squaring per bit, plus one accumulation per set bit). Each multiplication is on a fixed-size matrix.
The code
#include <array>#include <cstdint>
namespace {
using Matrix = std::array<std::array<std::int64_t, 2>, 2>;
Matrix multiply(const Matrix& a, const Matrix& b) { return {{ {a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]}, {a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]}, }};}
Matrix power(Matrix base, int exp) { Matrix result = {{{1, 0}, {0, 1}}}; // identity while (exp > 0) { if (exp & 1) result = multiply(result, base); base = multiply(base, base); exp >>= 1; } return result;}
} // namespace
int climbStairs(int n) { if (n <= 1) return 1; const Matrix M = {{{1, 1}, {1, 0}}}; const Matrix result = power(M, n); return static_cast<int>(result[0][0]);}Two helper functions: 2×2 multiply and fast power. The main entry is three lines once the helpers are in place.
When this is the right answer
- Astronomically large . For , the bottom-up loop is hopeless; matrix exponentiation evaluates the answer in 60 multiplications.
- Variants where the recurrence is more complex. Generalized step sets (
f(n) = f(n-1) + f(n-3) + f(n-7)) become matrices for some constant ; the same fast-power trick applies, and you still get — fast enough for any reasonable input. - Reading the question between the lines. If the interviewer asks “what if were a billion?” they’re handing you the matrix-exponentiation prompt.
When it’s overkill
- Standard interview problem with . Bottom-up is faster in practice for small because the constant factor is much smaller. Matrix exponentiation pays off only when is large enough that wins decisively over .
Complexity
- Time: multiplications, each on a fixed-size matrix.
- Space: for the recursion / iteration stack of repeated squaring.
Approach comparison
| Approach | Time | Space | When to reach for it |
|---|---|---|---|
| Naive recursion | Baseline only; never ship. | ||
| Memoized recursion | When the recurrence is irregular or sparse. | ||
| Bottom-up two-var | Default. Linear time, constant space. | ||
| Matrix exponentiation | Astronomically large or generalized recurrences. |
Default answer: the two-variable bottom-up sweep. State that out loud when you close.
Follow-ups to anticipate
| The interviewer asks… | What to do |
|---|---|
| ”What if you can take 1, 2, or 3 steps?” | Generalize the recurrence: . Same bottom-up sweep with three running variables. |
| ”What if the step set is an arbitrary array ?” | . Bottom-up with an array; per-step work is , so total is . |
| ”What if some stairs are broken (you can’t land on them)?” | Set explicitly, then run the same recurrence. The “ways through” each broken stair drop to zero. |
| ”What if you must take exactly hops to reach the top?“ | 2D DP: = ways to reach stair in exactly hops. Recurrence: . |
| ”What if the answer doesn’t fit in a 64-bit int?” | Big integer library, or modular arithmetic if the spec asks for the answer mod . The matrix-exp version handles modular arithmetic cleanly: every multiplication reduces mod . |
| ”Climbing stairs with a cost — minimize total cost.” | Different problem. The recurrence becomes . See LC 746. |
| ”What if you can also jump back (negative steps)?” | Now it’s a graph reachability problem; counting paths can produce infinite loops unless you bound the path length. |
| ”How do I prove the bottom-up answer is correct?” | Induction on . Base cases are direct; inductive step uses the recurrence. Same proof as Approach 01, but the iteration order makes the dependencies obvious. |
Pattern recognition — constant-state DP
The move this chapter teaches — evaluate a recurrence whose state is a fixed-size window over previous values — is a recurring shape in dynamic programming:
- House Robber. . Same two-variable collapse.
- Best Time to Buy/Sell Stock. Two running values: best profit so far and minimum price so far. Constant state, linear sweep.
- Maximum Subarray (Kadane’s). Two running values: current ending sum and best ever. Same shape.
- Decode Ways (LC 91). depends on and subject to a string-validity check. Same recurrence shape, decorated.
- Tribonacci. . Three-variable collapse instead of two; matrix exponentiation generalizes naturally.
- Domino tilings. Surprisingly often reduce to Fibonacci-like recurrences; the matrix-exponentiation trick handles huge boards.
The reflex: when a recurrence depends on a constant number of previous values, the DP can always be done in that number of variables — and matrix exponentiation gives a option if the input ranges demand it.
What to say out loud — the delivery script
Ten beats:
- “Let me restate: count the distinct ordered sequences of 1-step and 2-step hops that climb stairs.”
- Ask: “upper bound on ? hops just 1 and 2? does order matter?”
- “The recurrence: to reach stair , the last hop was either a 1 or a 2, so with . That’s Fibonacci.”
- “Naive recursion is — same subproblems get computed many times. Let me name that and move on.”
- “Memoization caches each on first computation: time, space.”
- “Bottom-up makes the iteration order explicit: fill from to . The key observation is that we only ever need the last two values, so the array collapses to two variables — space.”
- Write the bottom-up code — six lines. Trace : 1, 1, 2, 3, 5, 8.
- “Edge cases: and both return 1 directly; the loop just doesn’t execute. For large , switch to
long longto avoid overflow.” - “For the senior bonus: matrix exponentiation gives . The Fibonacci matrix raised to encodes and in its first row. Useful when is huge.”
- “For the standard interview, I’d ship the two-variable bottom-up. If the interviewer pushes on huge , I’d offer the matrix version.”
Summary
Climbing Stairs is the rite of passage for dynamic programming. The interview signal isn’t producing the optimal answer — it’s articulating the recurrence in one sentence, then walking the four-step optimization staircase: naive → memoized → bottom-up → matrix exponentiation. Each step teaches a different DP move, and every later DP problem reuses one or more of them.
The wider lesson is the constant-state recognition. Many DP problems have recurrences that depend on a fixed-size window of previous values — Fibonacci, House Robber, Kadane’s, decode-ways, tilings — and every one of them collapses to a fixed-size set of running variables. Internalize that shape here; the rest of the easier DP family becomes pattern matching.