Climbing Stairs
in the wild Counting paths through a state machine where each step has a fixed set of moves; the simplest 'cumulative count' DP that scales to any 1D recurrence.
Dynamic programming · 1D table fill
…
Problem
You're climbing a staircase with n steps. You can take 1 or 2 steps at a time. How many distinct ways can you climb to the top?
Examples
n = 2→2(1+1, or2)n = 3→3(1+1+1,1+2,2+1)n = 4→5(1+1+1+1,1+1+2,1+2+1,2+1+1,2+2)n = 5→8
If you stare at the answers — 2, 3, 5, 8 — that's Fibonacci. The proof: the last step you take is either a 1 (preceded by some way to reach n-1) or a 2 (preceded by some way to reach n-2). So f(n) = f(n-1) + f(n-2).
Why this is the canonical first DP problem
Three solutions, in increasing sophistication:
- Naïve recursion. Direct translation of
f(n) = f(n-1) + f(n-2)— but it re-solves the same subproblem exponentially many times.O(2^n). - Memoized recursion. Same shape, but cache each call.
O(n)time and space. - Tabulation with rolling variables. Throw away the recursion entirely. Walk
ifrom 2 to n, keeping only the last two values.O(n)time,O(1)space.
Climbing Stairs is the first DP problem every textbook teaches because it's the cleanest possible demonstration of the trade: the naïve recursion works but is unusably slow; memoization makes it fast; tabulation makes it both fast and tiny. Every other DP problem you'll see is some variant of this same pipeline.
Hints
Hint 1 — find the recurrence
What's the last step you took to reach n? It was either a 1 (coming from step n-1) or a 2 (coming from step n-2). So ways(n) = ways(n-1) + ways(n-2). What are the base cases?
Hint 2 — why naïve recursion explodes
To compute f(5) you need f(4) and f(3). To compute f(4) you need f(3) and f(2). Notice you compute f(3) twice. Then f(2) is computed 3 times. The tree of calls doubles at every level — exponential. The fix is caching.
Hint 3 — collapse to O(1) space
The recurrence only needs the previous two values. So instead of a full array, keep two integers a, b = 1, 2 and roll them: (a, b) = (b, a + b). After n - 1 iterations, b is the answer.
Solution
def climb_stairs(n: int) -> int:
# Base cases bake the recurrence f(n) = f(n-1) + f(n-2) into two rolling vars.
if n <= 2:
return n
a, b = 1, 2 # f(1) = 1, f(2) = 2
for _ in range(3, n + 1):
a, b = b, a + b # roll forward
return b
cases = [(1, 1), (2, 2), (3, 3), (4, 5), (5, 8), (10, 89), (45, 1836311903)]
for n, expected in cases:
got = climb_stairs(n)
mark = "✓" if got == expected else "✗"
print(f"{mark} climb_stairs({n}) = {got} (expected {expected})")function climbStairs(n) {
if (n <= 2) return n;
let a = 1, b = 2;
for (let i = 3; i <= n; i++) {
const next = a + b;
a = b;
b = next;
}
return b;
}
const cases = [[1, 1], [2, 2], [3, 3], [4, 5], [5, 8], [10, 89], [45, 1836311903]];
for (const [n, expected] of cases) {
const got = climbStairs(n);
console.log(`${got === expected ? "✓" : "✗"} climbStairs(${n}) = ${got} (expected ${expected})`);
}pub fn climb_stairs(n: u32) -> u64 {
if n <= 2 { return n as u64; }
let (mut a, mut b): (u64, u64) = (1, 2);
for _ in 3..=n {
let next = a + b;
a = b;
b = next;
}
b
}
fn main() {
let cases: &[(u32, u64)] = &[
(1, 1), (2, 2), (3, 3), (4, 5), (5, 8), (10, 89), (45, 1_836_311_903),
];
for (n, expected) in cases {
let got = climb_stairs(*n);
let mark = if got == *expected { "✓" } else { "✗" };
println!("{mark} climb_stairs({}) = {} (expected {})", n, got, expected);
}
}#include <cstdint>
#include <iostream>
std::uint64_t climb_stairs(int n) {
if (n <= 2) return n;
std::uint64_t a = 1, b = 2;
for (int i = 3; i <= n; ++i) {
std::uint64_t next = a + b;
a = b;
b = next;
}
return b;
}
int main() {
std::pair<int, std::uint64_t> cases[] = {
{1, 1}, {2, 2}, {3, 3}, {4, 5}, {5, 8}, {10, 89}, {45, 1836311903ULL},
};
for (auto& [n, expected] : cases) {
auto got = climb_stairs(n);
std::cout << (got == expected ? "✓" : "✗")
<< " climb_stairs(" << n << ") = " << got
<< " (expected " << expected << ")\n";
}
}#include <stdio.h>
#include <stdint.h>
uint64_t climb_stairs(int n) {
if (n <= 2) return (uint64_t)n;
uint64_t a = 1, b = 2;
for (int i = 3; i <= n; i++) {
uint64_t next = a + b;
a = b;
b = next;
}
return b;
}
int main(void) {
int ns [] = {1, 2, 3, 4, 5, 10, 45};
uint64_t exp[] = {1, 2, 3, 5, 8, 89, 1836311903ULL};
for (int i = 0; i < 7; i++) {
uint64_t got = climb_stairs(ns[i]);
printf("%s climb_stairs(%d) = %llu (expected %llu)\n",
got == exp[i] ? "✓" : "✗", ns[i],
(unsigned long long)got, (unsigned long long)exp[i]);
}
return 0;
}Three lines of state, one rolling update — that's it. The space-optimized form is more useful than it looks: every 1D DP problem whose recurrence depends on a constant-bounded history (Fibonacci, climb-stairs, house-robber, decode-ways) collapses to O(1) space the same way.
Complexity
- Time
- O(n) — one pass, O(1) work per step.
- Space
- O(1) — two rolling integers replace the full table.
- Naïve recursion
- O(2^n) — same subproblem solved exponentially many times. Memoization recovers O(n).
- O(log n) via matrix exponentiation
- Fibonacci can be computed in
O(log n)via[[1,1],[1,0]]^n. Rare to need unless n is enormous.
In the wild
- Counting paths in a finite-state machine with fixed per-step transitions — the literal generalization.
- Tiling problems. Count ways to tile a 2×n strip with 2×1 dominoes → Fibonacci.
- Decoding combinations. "Decode Ways" (leetcode 91): given a digit string, count how many ways to parse it as letters. Same Fibonacci-shape recurrence with one more branch.
- Cumulative state counting in robotics scenarios with bounded action sets.
Variations
- K-step climb. Each step can be 1, 2, …, k. Recurrence becomes
f(n) = sum of f(n-i) for i in 1..k. Same template, wider window. - Min-cost climb. Each step has a cost; minimize total. Recurrence:
f(n) = cost[n] + min(f(n-1), f(n-2)). - Distinct paths (output them). Backtracking instead of counting. Exponential blowup; DP only helps the count.
- House Robber. Same
f(n) = max(f(n-1), f(n-2) + value[n])shape. Identical solution skeleton.