Facebook Pixel

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:

  1. 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)
  2. Jump up to stair i + 2^jump. After using this operation, jump increases by 1.

    • First jump: move up by 2^0 = 1 stair, then jump becomes 1
    • Second jump: move up by 2^1 = 2 stairs, then jump becomes 2
    • Third jump: move up by 2^2 = 4 stairs, then jump becomes 3
    • And so on...

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Current position i - where Alice is on the staircase
  2. Last operation type j - whether the last move was down (1) or up (0)
  3. 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 us k + 1, not k
  • From k + 1, she must jump up (can't go down twice), which takes her even further from k

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 position
  • j 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:

  1. Going Down: If i > 0 (not at stair 0) and j == 0 (last operation wasn't going down), we can go down one stair. We recursively call dfs(i - 1, 1, jump) where:

    • New position is i - 1
    • Set j = 1 to mark that we just went down
    • Keep jump unchanged
  2. 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 shift 1 << jump for efficiency)
    • Set j = 0 to mark that we just jumped up
    • Increment jump by 1

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 Evaluator

Example 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:
    1. Jump up: go to stair 1 + 2^0 = 2, calling dfs(2, 0, 1)
    2. Go down: go to stair 0, calling dfs(0, 1, 0)

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:
    1. Jump up: go to stair 2 + 2^1 = 4, calling dfs(4, 0, 2)
    2. Go down: go to stair 1, calling dfs(1, 1, 1)

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 1
  • dfs(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 by k + 1 due to the pruning condition if 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 position i. Since we add 1 << jump (which is 2^jump) to position, to reach a position around k, we need at most O(log k) jumps
  • For each jump value from 0 to log k, we can reach at most O(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 most O(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 reach k by going down once
  • From position k + 2 or higher, Alice cannot reach k 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 on k 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):
    ...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More