Topics › Dynamic Programming › 1D DP
Climbing stairs
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
A staircase has n steps. Each move climbs either 1 step or 2 steps. Count the distinct sequences of moves that end exactly on step n. Order matters: climbing 1 + 2 and 2 + 1 are two different ways.
Constraints: 1 ≤ n ≤ 45 on LeetCode, where the answer fits in an int. The Java methods return long, which keeps them exact up to n = 91, and they also accept n = 0.
Example 1
n = 3
answer = 3 // 1+1+1, 1+2, 2+1
Example 2
n = 5
answer = 8 // grouped by how many 2-steps are used, see below
For n = 5 the sequences use zero 2-steps (1 way), one 2-step among four moves (4 ways), or two 2-steps among three moves (3 ways).
Approach 1: Recursion
Intuition. Counting all sequences directly is awkward, so look at the last move instead. Any sequence that ends on step n finishes with either a 1-step from step n - 1 or a 2-step from step n - 2. Those two groups are disjoint (their last moves differ) and together they contain every valid sequence. So
ways(n) = ways(n - 1) + ways(n - 2)
The base cases are ways(1) = 1 (a single 1-step) and ways(0) = 1. The second one surprises people: there is exactly one way to climb zero steps, the empty sequence. It is not a trick; it is what makes ways(2) = ways(1) + ways(0) = 2 come out right (1 + 1 and 2).
This is the Fibonacci recurrence with different starting values: ways(n) = F(n + 1). Recognising that is useful, but the reasoning above is what transfers to other problems, because it does not depend on knowing the sequence in advance.
Algorithm.
- If
n ≤ 1, return 1. - Otherwise return
ways(n - 1) + ways(n - 2).
static long recursive(int n) {
if (n <= 1) return 1; // step 0 or step 1: exactly one way
return recursive(n - 1) + recursive(n - 2);
}
Complexity. Time O(2^n): each call branches twice, and the same ways(i) is recomputed many times (the call count itself grows like Fibonacci). Space O(n) for the recursion depth.
At n = 45 this is about 3.7 billion calls (2 · ways(n) - 1 in general). The test file runs it only up to n = 30.
Approach 2: Memoization
Intuition. There are only n + 1 different questions the recursion can ask: ways(0) through ways(n). Store each answer in an array the first time it is computed, and every later request becomes a lookup.
Algorithm.
- Create
dpof sizen + 1, filled with-1(no real count is negative). - In the recursive helper, return 1 for
i ≤ 1, returndp[i]if it is filled, and otherwise computeways(i - 1) + ways(i - 2), store it indp[i], and return it.
static long memoization(int n) {
long[] dp = new long[n + 1];
Arrays.fill(dp, -1); // -1 = not computed yet
return ways(n, dp);
}
static long ways(int i, long[] dp) {
if (i <= 1) return 1;
if (dp[i] != -1) return dp[i];
return dp[i] = ways(i - 1, dp) + ways(i - 2, dp);
}
Complexity. Time O(n): each state is computed once with O(1) work. Space O(n) for the table plus O(n) for the recursion stack.
Approach 3: Tabulation
Intuition. The recurrence only looks downward, at smaller steps. So fill the table from the ground up: once dp[i - 1] and dp[i - 2] exist, dp[i] is one addition away. No recursion means no risk of a stack overflow for large n.
Algorithm.
dp[0] = 1,dp[1] = 1.- For
ifrom 2 ton, setdp[i] = dp[i - 1] + dp[i - 2]. - Return
dp[n].
The array has at least two cells so that n = 0 can still write dp[1].
static long tabulation(int n) {
long[] dp = new long[Math.max(n + 1, 2)];
dp[0] = 1; // standing on the ground: one way (do nothing)
dp[1] = 1; // a single 1-step
for (int i = 2; i <= n; i++) dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
Complexity. Time O(n), space O(n).
Approach 4: Space optimized
Intuition. Step 2 of the tabulation reads only the two cells just below the one it writes. Keep those two numbers in variables and slide them up the staircase.
Algorithm.
prev2 = 1(ways to reach step 0),prev = 1(ways to reach step 1).- For
ifrom 2 ton:cur = prev + prev2, thenprev2 = prev,prev = cur. - Return
prev, which isways(n). Forn ≤ 1the loop never runs andprev = 1is already right.
static long spaceOptimized(int n) {
long prev2 = 1, prev = 1; // ways(i - 2) and ways(i - 1), starting at i = 2
for (int i = 2; i <= n; i++) {
long cur = prev + prev2;
prev2 = prev;
prev = cur;
}
return prev;
}
Dry run for n = 5:
| i | prev2 before | prev before | cur | prev2 after | prev after |
|---|---|---|---|---|---|
| 2 | 1 | 1 | 2 | 1 | 2 |
| 3 | 1 | 2 | 3 | 2 | 3 |
| 4 | 2 | 3 | 5 | 3 | 5 |
| 5 | 3 | 5 | 8 | 5 | 8 |
The answer is prev = 8.
Complexity. Time O(n), space O(1).
Edge cases and pitfalls
n = 0andn = 1. Both return 1. Gettingways(0)wrong (returning 0) makes every larger answer wrong, because it feeds intoways(2).- Order matters. If
1 + 2and2 + 1counted as the same way, the answer would just be the number of possible 2-step counts,n / 2 + 1. The “last move” argument counts ordered sequences; counting unordered combinations needs a different DP (see Coin Change 2 below). - Overflow.
ways(45) = 1,836,311,903still fits inint, butways(46)does not. The methods uselong, exact throughways(91). - Recursion depth. The memoized version recurses
nlevels deep. Fine forn = 45, risky in Java fornin the tens of thousands; prefer the loop there. - Variants. If each move can climb 1, 2 or 3 steps, the recurrence gains a third term. If it can climb any number from 1 to
k, it becomes a sum over the lastkcells, which is the same shape as Frog Jump with K distances.
Related problems
- Introduction to DP walks through recursion, memoization, tabulation and space optimization on Fibonacci.
- Fibonacci Number is the same recurrence shifted by one.
- Frog Jump keeps the two possible last moves but minimises a cost instead of counting.
- Frog jump with K distances generalises the last move to any of
klengths. - Coin Change 2 (DP - 22) counts unordered combinations, the variant where
1 + 2and2 + 1are the same.