Determine the number of distinct ways to climb a staircase of n steps by taking either 1 or 2 steps at a time.
Input: n = 4
Output: 5
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
This is the Fibonacci sequence in disguise. The number of ways to reach step n equals the ways to reach n−1 (then take a single step) plus the ways to reach n−2 (then take a double step). Nature does the same counting when modelling rabbit populations — each month's count is the sum of the previous two.
The last move into step n was either a 1-step (from n−1) or a 2-step (from n−2). So ways(n) = ways(n−1) + ways(n−2). Since each answer only needs the previous two, we can compute it with two variables and one loop.
A brute force solution to this problem is to go through all possible combinations of moving 1 or 2 steps up the stairs until reaching the top. How would we do this? Think about how to get to stair i:
One thing we know for sure is that to reach step i, we need to reach it from either step i - 1, or step i - 2 since we can only climb 1 or 2 steps at a time:
This is all the information we need. If we want to know all the different ways we can get to step i, we just need to know:
i - 1 (climbing_stairs(i - 1)).i - 2 (climbing_stairs(i - 2)).This highlights that this problem has an optimal substructure where, in order to solve climbing_stairs(n), we need the answers to two of its subproblems. We can translate this to a recurrence relation:
climbing_stairs(n) = climbing_stairs(n - 1) + climbing_stairs(n - 2)
Let’s first implement this using recursion.
To do this, we’ll need to identify the base cases, which handle the simplest subproblems. The simplest versions of this problem occur when the number of steps is 1 or 2. If n equals 1, we return 1 since the only way to reach step 1 is to climb 1 step. If n equals 2, return 2 since there are two ways to reach step 2.
If we apply this recursive logic to a staircase of 6 steps, this is what the recursion tree would look like:
This solution is considered a top-down solution as it starts from the main problem, and recursively breaks it down into smaller subproblems as it progresses down the recursive tree.
You may have noticed in the recursion tree that we do some repeated work by calling the same subproblem multiple times (e.g., climbing_stairs(4) is called twice). This highlights the existence of overlapping subproblems. This isn't a big issue for short staircases, but for a taller one with more steps, it can result in a lot of repeated calculations of subproblems we’ve already solved. This is where memoization comes into play.
Memoization
Storing the result of each subproblem the first time we solve it, then reusing these stored results when needed, is a technique known as memoization. For example, after we calculate the subproblem of n = 3 (climbing_stairs(3)) for the first time, we don’t need to calculate it again; we can just fetch the already-calculated result for n = 3. The same applies to n = 4. This greatly reduces the size of the recursion tree:
We use a hash map for memoization to store the results of subproblems for constant-time access. For example, after calculating the result for the subproblem n = 3, we store the result in the hash map as a value, where the key is 3.
As we can see, we’ve successfully implemented a DP solution using top-down memoization. We identified the subproblems, used them to create the recurrence relation, specified our base cases, and applied memoization to ensure each subproblem is solved only once.
memo = {}
def climbing_stairs_top_down(n: int) -> int:
# Base cases: With a 1-step staircase, there’s only one way to climb it.
# With a 2-step staircase, there are two ways to climb it.
if n <= 2:
return n
if n in memo:
return memo[n]
# The number of ways to climb to the n-th step is equal to the sum of the number
# of ways to climb to step n - 1 and to n - 2.
memo[n] = climbing_stairs_top_down(n - 1) + climbing_stairs_top_down(n - 2)
return memo[n]// Return type is long long: the count grows like Fibonacci and overflows a
// 32-bit int around n = 47.
long long climbingStairsTopDown(int n) {
if (n <= 2) { // base cases
return n;
}
// Arrive from step n-1 (a single) or step n-2 (a double).
return climbingStairsTopDown(n - 1) + climbingStairsTopDown(n - 2);
}// These methods live inside a class, e.g. class Solution { ... }
// Return long: the count grows like Fibonacci and overflows int around n = 47.
long climbingStairsTopDown(int n) {
if (n <= 2) { // base cases
return n;
}
// Arrive from step n-1 (a single) or step n-2 (a double).
return climbingStairsTopDown(n - 1) + climbingStairsTopDown(n - 2);
}Time complexity:
climbing_stairs_top_down is because the depth of the recursion tree is n, and its branching factor is 2 since we make 2 recursive calls at each point in the tree.Space complexity: The space complexity is due to the recursive call stack, which grows to a height of . The memoization array also contributes to the space occupied by storing key-value pairs.
Generally, any problem that can be solved using top-down memoization can also be solved using a bottom-up DP approach, where we translate the memoization array to a DP array. Let's explore how this works.
Translating the memoization array to a DP array
Think about what each value in the DP array represents. We want this array to store the answers to our subproblems (i.e., dp[i] should store the number of ways we can reach step i). Now, remember that our memoization array stores the same thing. In other words, dp[i] and memo[i] store the same result.
In our top-down implementation, the memoization stores results like so:
memo[n] = climbing_stairs(n - 1) + climbing_stairs(n - 2)
However, if the results of climbing_stairs(n - 1) and climbing_stairs(n - 2) were already calculated and memoized, this is what would actually be going on:
memo[n] = memo[n - 1] + memo[n - 2]
Now that we have this tabular relationship for the memoization array, simply change “memo” to “dp” to get the DP relationship:
dp[n] = dp[n - 1] + dp[n - 2]
We call this a bottom-up solution because we need to calculate the solutions to smaller subproblems before we can solve the larger ones. In other words, we "build up" to the main solution as opposed to the top-down solution, where we start with the main problem, n, and work our way down.
Base cases
Our base cases stay the same: the answers to dp[1] and dp[2] are 1 and 2, respectively.
Return statement
In our top-down solution, we return memo[n]. Since there’s a one-to-one relationship between the memoization array and the DP array, we can just return dp[n] in our bottom-up solution.
def climbing_stairs_bottom_up(n: int) -> int:
if n <= 2:
return n
dp = [0] * (n + 1)
# Base cases.
dp[1], dp[2] = 1, 2
# Starting from step 3, calculate the number of ways to reach each step until the
# n-th step.
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
Time complexity: The time complexity of climbing_stairs_bottom_up is as we iterate through elements of the DP array.
Space complexity: The space complexity is due to the space taken up by the DP array, which contains elements.
An important thing to notice is that in the DP solution, we only ever need to access the previous two values of the DP array (at i - 1 and i - 2) to calculate the current value (at i). This means we don’t need to store the entire DP array.
Instead, we can use two variables to keep track of the previous two values:
one_step_before: to store the value of dp[i - 1].two_steps_before: to store the value of dp[i - 2].As we iterate through the steps, we update these two variables to always hold the values for the previous two steps. This approach retains the time complexity of , while reducing space complexity to . The adjusted implementation is below:
def climbing_stairs_bottom_up_optimized(n: int) -> int:
if n <= 2:
return n
# Initialize 'one_step_before' and 'two_steps_before' with the base cases.
one_step_before, two_steps_before = 2, 1
for i in range(3, n + 1):
# Calculate the number of ways to reach the current step.
current = one_step_before + two_steps_before
# Update the values for the next iteration.
two_steps_before = one_step_before
one_step_before = current
return one_step_beforelong long climbingStairsBottomUpOptimized(int n) {
if (n <= 2) {
return n;
}
long long twoBelow = 1; // ways to reach step 1
long long oneBelow = 2; // ways to reach step 2
for (int i = 3; i <= n; i++) {
long long current = oneBelow + twoBelow; // ways to reach step i
twoBelow = oneBelow; // slide the window up
oneBelow = current;
}
return oneBelow;
}// These methods live inside a class, e.g. class Solution { ... }
long climbingStairsBottomUpOptimized(int n) {
if (n <= 2) {
return n;
}
long twoBelow = 1; // ways to reach step 1
long oneBelow = 2; // ways to reach step 2
for (int i = 3; i <= n; i++) {
long current = oneBelow + twoBelow; // ways to reach step i
twoBelow = oneBelow; // slide the window up
oneBelow = current;
}
return oneBelow;
}Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
The two cells are two_below (blue) and one_below (green). Each step computes their sum, then slides right.
two_below = 1 (step 1), one_below = 2 (step 2).
current = 2 + 1 = 3. Slide: two_below = 2, one_below = 3.
current = 3 + 2 = 5. Slide: two_below = 3, one_below = 5 → answer 5.
| i | two_below | one_below | current = sum |
|---|---|---|---|
| start | 1 | 2 | — |
| 3 | 1→2 | 2→3 | 3 |
| 4 | 2→3 | 3→5 | 5 → return 5 |
Four steps → 5 ways, matching the enumeration at the top. ✓
With n = 2 the guard if n <= 2: return n fires, so we return 2 without ever seeding or looping. The two ways are 1+1 and 2.
| n | n ≤ 2 ? | action |
|---|---|---|
| 2 | yes | return n = 2 |
Result 2. If we skipped this guard, the loop from 3 would never run and one_below would still be correct — but the guard also protects n = 1 and keeps the seeds valid. ✗
Python's big integers never overflow, so the plain recursion is only a speed problem, not a correctness one.
Use long long for the running values; the count passes 231 near n = 47.
Use long for the same reason. Everything else is identical to the C++ version.
Tip: If you're having trouble coming up with the bottom-up solution, try starting with the top-down solution.
Designing a top-down solution first is often easier because we can first identify the recurrence relation, and then apply memoization to optimize it. The bottom-up solution, on the other hand, requires considering both steps at the same time. In addition, a bottom-up solution starts by solving subproblems first, which can be less intuitive, whereas a top-down solution starts with the main problem before working downward.
Once you have a working top-down solution, you can translate it into a bottom-up solution as described in the intuition above. Over time, you'll get better at mapping a recurrence relation directly to a bottom-up tabular relation, allowing you to skip the top-down approach.