3154. Find Number of Ways to Reach the K-th Stair
Problem Description
You have an infinite staircase where stairs are numbered starting from 0. Alice starts on stair 1 and wants to reach stair k
.
Alice has a variable called jump
that starts at 0. From any stair i
, she can perform one of two operations:
-
Go down one stair to stair
i - 1
. This operation has restrictions:- Cannot be used on stair 0 (since there's no stair -1)
- Cannot be used twice in a row (consecutively)
-
Jump up to stair
i + 2^jump
. After using this operation,jump
increases by 1.- First jump: move up by
2^0 = 1
stair, thenjump
becomes 1 - Second jump: move up by
2^1 = 2
stairs, thenjump
becomes 2 - Third jump: move up by
2^2 = 4
stairs, thenjump
becomes 3 - And so on...
- First jump: move up by
The task is to find the total number of different ways Alice can reach stair k
using any sequence of these operations.
Important notes:
- Alice can reach stair
k
multiple times in the same sequence, and each time counts toward the total - She can continue performing operations even after reaching stair
k
and potentially return to it again - The input
k
is a non-negative integer
For example, if k = 1
, Alice is already at stair 1, so that's one way. She could also jump up (to stair 2), go down (to stair 1), achieving another way to reach stair 1.
Intuition
The key insight is that Alice's movement follows a very specific pattern due to the constraints. Since she can't go down twice in a row, every downward move must be followed by an upward jump. This creates a decision tree where at each position, we need to track:
- Current position
i
- where Alice is on the staircase - Last operation type
j
- whether the last move was down (1) or up (0) - Jump counter
jump
- how many upward jumps have been made
The reason we need to track the last operation is crucial: if we just went down (j = 1
), we cannot go down again immediately. This constraint shapes our entire exploration strategy.
Notice that the upward jumps grow exponentially (2^0, 2^1, 2^2, ...
). This means Alice can quickly reach very high stairs. However, there's an important boundary to consider: if Alice is already at a position i > k + 1
, she cannot reach stair k
again. Why? Because:
- She can only go down by 1 at most (to
i - 1
) - Even if at
i = k + 2
, going down gives usk + 1
, notk
- From
k + 1
, she must jump up (can't go down twice), which takes her even further fromk
This gives us a natural pruning condition: stop exploring when i > k + 1
.
The recursive structure emerges naturally:
- From any valid position, we can always jump up (adding
2^jump
to our position) - We can only go down if we're not at stair 0 and our last move wasn't already down
Each time we land exactly on stair k
, we count it as one valid way. But we don't stop there - Alice can continue moving and potentially return to k
again through a different sequence of moves.
Memoization is essential here because the same state (i, j, jump)
can be reached through different sequences of operations. By caching these results, we avoid recalculating the number of ways from the same state multiple times.
Learn more about Memoization, Math, Dynamic Programming and Combinatorics patterns.
Solution Approach
The solution uses memoization search (dynamic programming with recursion) to explore all possible paths Alice can take to reach stair k
.
We implement a recursive function dfs(i, j, jump)
where:
i
represents the current stair positionj
indicates whether the last operation was going down (1) or not (0)jump
tracks how many upward jumps have been performed
Base Case and Pruning:
The function first checks if i > k + 1
. If true, it returns 0 because it's impossible to reach stair k
from this position (as explained in the intuition).
Counting Valid Paths:
When we're at position i
, we check if i == k
. If yes, we've found one way to reach stair k
, so we initialize ans = 1
. Otherwise, ans = 0
.
Recursive Exploration: From the current position, we explore two possible operations:
-
Going Down: If
i > 0
(not at stair 0) andj == 0
(last operation wasn't going down), we can go down one stair. We recursively calldfs(i - 1, 1, jump)
where:- New position is
i - 1
- Set
j = 1
to mark that we just went down - Keep
jump
unchanged
- New position is
-
Jumping Up: We can always jump up (no restrictions). We recursively call
dfs(i + (1 << jump), 0, jump + 1)
where:- New position is
i + 2^jump
(using bit shift1 << jump
for efficiency) - Set
j = 0
to mark that we just jumped up - Increment
jump
by 1
- New position is
Memoization:
The @cache
decorator automatically memoizes the function results. When dfs(i, j, jump)
is called with the same parameters again, it returns the cached result instead of recalculating.
Starting Point:
The solution begins by calling dfs(1, 0, 0)
:
- Start at stair 1
- No previous down operation (
j = 0
) - No jumps performed yet (
jump = 0
)
The function accumulates all possible ways by adding up the results from both recursive branches, ultimately returning the total number of ways to reach stair k
.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the solution for k = 2
.
Starting at stair 1 with jump = 0
and no previous down operation:
Initial call: dfs(1, 0, 0)
- Currently at stair 1, which is not equal to k=2, so
ans = 0
- We can explore two operations:
- Jump up: go to stair 1 + 2^0 = 2, calling
dfs(2, 0, 1)
- Go down: go to stair 0, calling
dfs(0, 1, 0)
- Jump up: go to stair 1 + 2^0 = 2, calling
Branch 1: dfs(2, 0, 1)
(jumped to stair 2)
- Currently at stair 2, which equals k=2, so
ans = 1
(found one way!) - We can explore two operations:
- Jump up: go to stair 2 + 2^1 = 4, calling
dfs(4, 0, 2)
- Go down: go to stair 1, calling
dfs(1, 1, 1)
- Jump up: go to stair 2 + 2^1 = 4, calling
Branch 1.1: dfs(4, 0, 2)
(at stair 4)
- Position 4 > k+1 (which is 3), so return 0 immediately
Branch 1.2: dfs(1, 1, 1)
(went down to stair 1)
- Currently at stair 1, not equal to k=2, so
ans = 0
- Last operation was down (j=1), so we can't go down again
- Can only jump up: go to stair 1 + 2^1 = 3, calling
dfs(3, 0, 2)
Branch 1.2.1: dfs(3, 0, 2)
(at stair 3)
- Currently at stair 3, not equal to k=2, so
ans = 0
- Can go down to stair 2, calling
dfs(2, 1, 2)
- Can jump up to stair 3 + 2^2 = 7, calling
dfs(7, 0, 3)
Branch 1.2.1.1: dfs(2, 1, 2)
(went down to stair 2)
- Currently at stair 2, which equals k=2, so
ans = 1
(found another way!) - Last operation was down, so can only jump up
- Jump to stair 2 + 2^2 = 6, calling
dfs(6, 0, 3)
- This returns 0 (position > k+1)
- Total from this branch: 1
Branch 1.2.1.2: dfs(7, 0, 3)
- Position 7 > k+1, return 0
Going back up the call stack:
dfs(3, 0, 2)
returns 1 (from going down to stair 2)dfs(1, 1, 1)
returns 1dfs(2, 0, 1)
returns 1 + 0 + 1 = 2
Branch 2: dfs(0, 1, 0)
(went down to stair 0)
- Currently at stair 0, not equal to k=2, so
ans = 0
- Can't go down from stair 0 (would be negative)
- Can only jump up: go to stair 0 + 2^0 = 1, calling
dfs(1, 0, 1)
This dfs(1, 0, 1)
call explores similar paths but with jump counter at 1, finding additional ways to reach k=2.
The final answer accumulates all these different paths where Alice lands on stair 2.
Solution Implementation
1class Solution:
2 def waysToReachStair(self, k: int) -> int:
3 from functools import cache
4
5 @cache
6 def dfs(current_stair: int, can_go_down: int, jump_power: int) -> int:
7 """
8 Dynamic programming function to count ways to reach stair k.
9
10 Args:
11 current_stair: Current position on the staircase
12 can_go_down: Flag indicating if we can move down (0 = can move down, 1 = cannot)
13 jump_power: Current power for the jump operation (jump size = 2^jump_power)
14
15 Returns:
16 Number of ways to reach stair k from current position
17 """
18 # Pruning: if current position is too far from target, no valid paths exist
19 if current_stair > k + 1:
20 return 0
21
22 # Initialize answer: add 1 if we're currently at the target stair
23 ways_count = int(current_stair == k)
24
25 # Option 1: Move down by 1 stair (only if we're above stair 0 and allowed to go down)
26 if current_stair > 0 and can_go_down == 0:
27 # After moving down, set flag to 1 (cannot move down consecutively)
28 ways_count += dfs(current_stair - 1, 1, jump_power)
29
30 # Option 2: Jump up by 2^jump_power stairs
31 # After jumping up, reset flag to 0 (can move down again) and increment jump power
32 ways_count += dfs(current_stair + (1 << jump_power), 0, jump_power + 1)
33
34 return ways_count
35
36 # Start from stair 1, can move down initially, with jump power 0
37 return dfs(1, 0, 0)
38
1class Solution {
2 // Memoization cache to store computed results
3 // Key encoding: (stairPosition << 32) | (jumpCount << 1) | previousMoveWasDown
4 private Map<Long, Integer> memo = new HashMap<>();
5 private int targetStair;
6
7 /**
8 * Calculates the number of ways to reach stair k from stair 1.
9 * Movement rules:
10 * - Can move down 1 stair (if not at stair 0 and didn't just move down)
11 * - Can jump up by 2^jumpCount stairs (jumpCount increases after each jump)
12 *
13 * @param k The target stair number to reach
14 * @return The number of distinct ways to reach stair k
15 */
16 public int waysToReachStair(int k) {
17 this.targetStair = k;
18 // Start at stair 1, with no previous down move, and jump count 0
19 return dfs(1, 0, 0);
20 }
21
22 /**
23 * Recursive DFS with memoization to count ways to reach target stair.
24 *
25 * @param currentStair The current stair position
26 * @param previousMoveWasDown Flag indicating if the last move was down (1) or not (0)
27 * @param jumpCount The current jump count (determines next jump distance: 2^jumpCount)
28 * @return Number of ways to reach target stair from current state
29 */
30 private int dfs(int currentStair, int previousMoveWasDown, int jumpCount) {
31 // Pruning: if we're too far above target, no way to reach it
32 // (can only go down by 1 at a time, so k+1 is the maximum useful position)
33 if (currentStair > targetStair + 1) {
34 return 0;
35 }
36
37 // Create unique key for memoization
38 // Encode state into a single long value for efficient storage
39 long stateKey = ((long) currentStair << 32) | (jumpCount << 1) | previousMoveWasDown;
40
41 // Check if this state has been computed before
42 if (memo.containsKey(stateKey)) {
43 return memo.get(stateKey);
44 }
45
46 // Initialize count: if we're at target stair, this is one valid way
47 int waysCount = (currentStair == targetStair) ? 1 : 0;
48
49 // Option 1: Move down one stair
50 // Only allowed if we're above stair 0 and didn't just move down
51 if (currentStair > 0 && previousMoveWasDown == 0) {
52 waysCount += dfs(currentStair - 1, 1, jumpCount);
53 }
54
55 // Option 2: Jump up by 2^jumpCount stairs
56 // Always allowed, and increases jump count for next jump
57 int jumpDistance = 1 << jumpCount; // 2^jumpCount
58 waysCount += dfs(currentStair + jumpDistance, 0, jumpCount + 1);
59
60 // Store result in memoization cache
61 memo.put(stateKey, waysCount);
62 return waysCount;
63 }
64}
65
1class Solution {
2public:
3 int waysToReachStair(int k) {
4 // Memoization map to store computed states
5 // Key encoding: (currentStair << 32) | (jumpCount << 1) | canGoBack
6 unordered_map<long long, int> memo;
7
8 // Recursive DFS function to count ways to reach stair k
9 // Parameters:
10 // - currentStair: current position on the stairs
11 // - canGoBack: flag indicating if we can still go back (0 = can go back, 1 = cannot)
12 // - jumpCount: number of jumps made so far (determines next jump size)
13 auto dfs = [&](this auto&& dfs, int currentStair, int canGoBack, int jumpCount) -> int {
14 // Pruning: if current position exceeds k+1, no valid path exists
15 if (currentStair > k + 1) {
16 return 0;
17 }
18
19 // Create unique key for memoization
20 long long stateKey = ((long long)currentStair << 32) | (jumpCount << 1) | canGoBack;
21
22 // Check if this state has been computed before
23 if (memo.contains(stateKey)) {
24 return memo[stateKey];
25 }
26
27 // Initialize result: if we're at target stair k, count this as one way
28 int waysCount = (currentStair == k) ? 1 : 0;
29
30 // Option 1: Go back one stair (only if we haven't gone back consecutively)
31 if (currentStair > 0 && canGoBack == 0) {
32 waysCount += dfs(currentStair - 1, 1, jumpCount);
33 }
34
35 // Option 2: Jump forward by 2^jumpCount stairs
36 int jumpDistance = 1 << jumpCount; // Calculate 2^jumpCount
37 waysCount += dfs(currentStair + jumpDistance, 0, jumpCount + 1);
38
39 // Store result in memoization map
40 memo[stateKey] = waysCount;
41
42 return waysCount;
43 };
44
45 // Start DFS from stair 1, with ability to go back, and 0 jumps made
46 return dfs(1, 0, 0);
47 }
48};
49
1/**
2 * Counts the number of ways to reach stair k starting from stair 1
3 * @param k - The target stair number
4 * @returns The number of different ways to reach stair k
5 */
6function waysToReachStair(k: number): number {
7 // Memoization map to store computed results
8 // Key encoding: (stairPosition << 32) | (jumpCount << 1) | canGoBackFlag
9 const memoCache: Map<bigint, number> = new Map();
10
11 /**
12 * Depth-first search to explore all possible paths
13 * @param currentStair - Current position on the stairs
14 * @param canGoBack - Flag indicating if we can go back one stair (0 = can go back, 1 = cannot)
15 * @param jumpCount - Number of jumps performed so far
16 * @returns Number of ways to reach target stair from current state
17 */
18 const dfs = (currentStair: number, canGoBack: number, jumpCount: number): number => {
19 // Pruning: if current position exceeds k+1, no valid path exists
20 if (currentStair > k + 1) {
21 return 0;
22 }
23
24 // Create unique key for memoization
25 const stateKey: bigint = (BigInt(currentStair) << BigInt(32)) | BigInt(jumpCount << 1) | BigInt(canGoBack);
26
27 // Return cached result if already computed
28 if (memoCache.has(stateKey)) {
29 return memoCache.get(stateKey)!;
30 }
31
32 let waysCount: number = 0;
33
34 // If we've reached the target stair, increment ways count
35 if (currentStair === k) {
36 waysCount++;
37 }
38
39 // Option 1: Go back one stair (only if allowed and not at stair 0)
40 if (currentStair > 0 && canGoBack === 0) {
41 waysCount += dfs(currentStair - 1, 1, jumpCount);
42 }
43
44 // Option 2: Jump forward by 2^jumpCount stairs
45 const jumpDistance: number = 1 << jumpCount;
46 waysCount += dfs(currentStair + jumpDistance, 0, jumpCount + 1);
47
48 // Cache the result before returning
49 memoCache.set(stateKey, waysCount);
50 return waysCount;
51 };
52
53 // Start from stair 1, can go back initially, with 0 jumps performed
54 return dfs(1, 0, 0);
55}
56
Time and Space Complexity
The time complexity is O(log^2 k)
and the space complexity is O(log^2 k)
.
Time Complexity Analysis:
- The function uses memoization with
@cache
decorator to avoid redundant calculations - The state space is determined by three parameters:
(i, j, jump)
- The value of
i
is bounded byk + 1
due to the pruning conditionif i > k + 1: return 0
- The value of
j
can only be 0 or 1 - The key insight is that
jump
determines how we reach positioni
. Since we add1 << jump
(which is2^jump
) to position, to reach a position aroundk
, we need at mostO(log k)
jumps - For each jump value from 0 to
log k
, we can reach at mostO(log k)
different positions through combinations of operations - The total number of unique states is
O(log k) Ă— 2 Ă— O(log k) = O(log^2 k)
- Each state is computed once due to memoization, resulting in
O(log^2 k)
time complexity
Space Complexity Analysis:
- The space is primarily used by the memoization cache and the recursion call stack
- The memoization cache stores all unique states visited, which is
O(log^2 k)
as analyzed above - The maximum recursion depth is
O(log k)
since we can have at mostO(log k)
jump operations in a path - Therefore, the total space complexity is
O(log^2 k)
dominated by the cache storage
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Pruning Condition
A critical pitfall is using an incorrect or overly aggressive pruning condition. Many might think to prune when current_stair > k
, but this would miss valid paths where Alice goes above stair k
and then comes back down.
Why k + 1
is the correct bound:
- From position
k + 1
, Alice can reachk
by going down once - From position
k + 2
or higher, Alice cannot reachk
using only down operations (limited to one consecutive down move) - The minimum jump size keeps increasing (1, 2, 4, 8...), so once we're at
k + 2
or higher, we can never land exactly onk
again
Incorrect approach:
if current_stair > k: # Too aggressive - misses valid paths! return 0
Correct approach:
if current_stair > k + 1: # Allows reaching k from k+1 by going down return 0
2. Misunderstanding the "Can Go Down" Flag
The flag parameter can be confusing. Some might interpret can_go_down = 1
as "allowed to go down", but it actually means the opposite.
Common mistake:
# Wrong interpretation if current_stair > 0 and can_go_down == 1: # Incorrect logic! ways_count += dfs(current_stair - 1, 0, jump_power)
Correct implementation:
# can_go_down: 0 = allowed to go down, 1 = not allowed (just went down) if current_stair > 0 and can_go_down == 0: ways_count += dfs(current_stair - 1, 1, jump_power) # Set to 1 after going down
3. Forgetting to Count Current Position
A subtle pitfall is forgetting to count the current position when it equals k
. Each time we reach stair k
, it should be counted as one valid way, even if we continue exploring from there.
Incorrect:
ways_count = 0 # Forgot to check if current_stair == k!
Correct:
ways_count = int(current_stair == k) # Count this visit if we're at k
4. Integer Overflow with Large Jump Powers
For large values of jump_power
, calculating 2^jump_power
could theoretically cause issues. While Python handles big integers well, in other languages this could overflow.
Potential issue in other languages:
# In languages with fixed integer sizes, this might overflow current_stair + (1 << jump_power)
Solution for other languages:
- Add overflow checks or use appropriate data types (e.g.,
long long
in C++) - Consider early termination when jump size exceeds reasonable bounds
5. Not Using Memoization
Without memoization, the solution would have exponential time complexity due to recalculating the same states multiple times.
Without memoization (inefficient):
def dfs(current_stair, can_go_down, jump_power):
# No caching - recalculates same states repeatedly
...
With memoization (efficient):
@cache # Essential for avoiding timeout!
def dfs(current_stair, can_go_down, jump_power):
...
Which technique can we use to find the middle of a linked list?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!