TL;DR
The answer is the Fibonacci number f(n) with f(1)=1, f(2)=2; compute it bottom-up with two rolling variables β O(n) time, O(1) space.
The recurrence
Let f(k) be the number of distinct ways to reach step k. The final move onto step k is either a 1-step from k-1 or a 2-step from k-2, and those path sets are disjoint, so:
f(k) = f(k-1) + f(k-2)
f(0) = 1 # one way to "stand at the ground": do nothing
f(1) = 1
Approach 1 β Brute-force recursion
Directly translate the recurrence. Each call branches into two.
class Solution:
def climbStairs(self, n: int) -> int:
def ways(k: int) -> int:
if k <= 1:
return 1
return ways(k - 1) + ways(k - 2)
return ways(n)
Complexity: O(2^n) time (the call tree is itself a Fibonacci tree), O(n) stack space.
Why the constraints kill it: at n = 45 this makes on the order of 2Β·10^9 calls β seconds to minutes β because the same subproblems (ways(k) for each k) are recomputed exponentially many times.
Approach 2 β Top-down memoization
The insight: there are only n distinct subproblems, ways(0) β¦ ways(n). Cache each the first time it is computed and every later request is O(1).
from functools import cache
class Solution:
def climbStairs(self, n: int) -> int:
@cache
def ways(k: int) -> int:
if k <= 1:
return 1
return ways(k - 1) + ways(k - 2)
return ways(n)
Walkthrough (n = 5): ways(5) needs ways(4)+ways(3). ways(4) computes ways(3)+ways(2); when ways(5) later asks for ways(3) it is already cached. Filled values are ways(2)=2, ways(3)=3, ways(4)=5, ways(5)=8 β 8.
Complexity: O(n) time (each ways(k) bodies runs once), O(n) space for cache + stack.
Approach 3 β Bottom-up tabulation
The insight: compute the subproblems in increasing order of k so every dependency is ready before it is needed β no recursion, no cache lookups.
class Solution:
def climbStairs(self, n: int) -> int:
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
for k in range(2, n + 1):
dp[k] = dp[k - 1] + dp[k - 2]
return dp[n]
Complexity: O(n) time, O(n) space for the table.
Approach 4 β Space-optimized (rolling variables)
The insight: dp[k] only ever reads dp[k-1] and dp[k-2]. Keep just those two numbers and slide them forward; the full array is unnecessary.
class Solution:
def climbStairs(self, n: int) -> int:
prev, curr = 1, 1 # f(0), f(1)
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
Walkthrough (n = 5): start (prev, curr) = (1, 1). Iterations: (1,2) β (2,3) β (3,5) β (5,8). Return curr = 8.
Complexity: O(n) time, O(1) space β the optimal version.
Common pitfalls
- Base cases off by one.
f(1) is 1, not 2. A common slip is seeding f(2) = 3; keep f(0) = f(1) = 1 and let the loop derive the rest.
- Returning
curr when n = 1. With the two-variable version above, n = 1 skips the loop and returns the initial curr = 1, which is correct β but only because both variables were seeded to 1. Double-check the tiny cases.
- Using plain recursion under a tight limit. Correct but exponential; always add memoization or go bottom-up.
Pattern takeaway
This is the βhello worldβ of 1-D DP: define f(k) over a single index, split on the last decision (which step size finished the path), and you get a linear recurrence. Whenever the answer at n decomposes into a fixed set of smaller indices, cache those subproblems β and if each state reads only a constant-width window behind it, collapse the table to a handful of rolling variables for O(1) space.