Facebook Pixel

3154. Find Number of Ways to Reach the K-th Stair


Problem Description

You are given a non-negative integer k. There exists a staircase with an infinite number of stairs, with the lowest stair numbered 0.

Alice has an integer jump, with an initial value of 0. She starts on stair 1 and wants to reach stair k using any number of operations. If she is on stair i, in one operation she can:

  • Go down to stair i - 1. This operation cannot be used consecutively or on stair 0.
  • Go up to stair i + 2^jump. And then, jump becomes jump + 1.

Return the total number of ways Alice can reach stair k.

Note: It is possible that Alice reaches the stair k, and performs some operations to reach the stair k again.

Intuition

To solve the problem, we utilize a recursive approach combined with memoization. The key idea is to define a function dfs(i, j, jump) that tracks the current stair i, last operation type j (0 for move up or 1 for move down), and the current jump value. This function computes the number of ways to reach stair k from stair i.

The reasoning process involves:

  • If the current stair i surpasses the stair k + 1, it's impossible to reach k again due to the restriction on consecutive moves downwards. Thus, return 0 in such cases.

  • When Alice is exactly on stair k, we have found a successful path, so include this as one way.

  • If Alice is not on stair 0 and just moved up (i.e., j is 0), she is allowed to move down and we call dfs with the updated stair and parameters.

  • Allow Alice to move upwards via i + 2^jump to explore other possible paths to reach stair k.

By employing memoization, we can store the results of previously computed states to avoid redundant calculations and reduce execution time. This approach efficiently explores all possible pathways Alice can take to reach stair k.

Learn more about Memoization, Math, Dynamic Programming and Combinatorics patterns.

Solution Approach

The solution leverages a recursive depth-first search (dfs) combined with memoization to efficiently compute the number of ways Alice can reach the target stair k. The function dfs(i, j, jump) is the core component, defined as follows:

  • Parameters:

    • i: Represents the current stair Alice is on.
    • j: Denotes the last operation type used (0 if the last move was up, 1 if the last move was down).
    • jump: Stores the current jump value, which determines the step size for upward movement.
  • Base Case:

    • If i exceeds k + 1, return 0 since Alice cannot go back down consecutively to reach k from beyond k + 1.
  • Target Reached:

    • If i equals k, consider this a valid path, thus initializing ans to 1 and continue to explore other potential paths.
  • Recursive Exploration:

    • When i > 0 and the previous operation was not a downward move (j == 0), Alice can move downwards to i - 1. We recursively call dfs(i - 1, 1, jump) to explore this path.
    • For upward movement, compute the new stair position as i + (1 << jump) representing 2^jump steps up, and increment jump for subsequent operations. This is solved with dfs(i + (1 << jump), 0, jump + 1).

The recursion accumulates paths reaching k and memoization avoids recalculating results for states that have previously been computed, significantly enhancing efficiency. The result of all possible valid paths is finally returned by the initial call dfs(1, 0, 0).

Here is the implementation:

class Solution:
    def waysToReachStair(self, k: int) -> int:
        @cache
        def dfs(i: int, j: int, jump: int) -> int:
            if i > k + 1:
                return 0
            ans = int(i == k)
            if i > 0 and j == 0:
                ans += dfs(i - 1, 1, jump)
            ans += dfs(i + (1 << jump), 0, jump + 1)
            return ans

        return dfs(1, 0, 0)

This approach ensures a comprehensive search through potential moves while optimizing performance using memoization.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's illustrate the solution with a small example:

Consider k = 3. Alice begins on stair 1 with jump initially 0.

  • Initial State:

    • Stair: 1
    • Last Operation: Up (denoted as j = 0)
    • Jump Value: 0
  • Step-by-Step Execution:

    1. From Stair 1: Alice can move:
      • Up to Stair 2: Jump value becomes 1 (2^0 = 1). Call dfs(2, 0, 1).
    2. From Stair 2:
      • Up to Stair 4: Jump value becomes 2 (2^1 = 2). Call dfs(4, 0, 2).
        • At this point, since stair 4 exceeds stair k + 1 = 4, no valid paths result from further calls. This path yields zero additional ways.
      • Down to Stair 1: Call dfs(1, 1, 1).
    3. From Stair 1 with Downward Constraint:
      • Up to Stair 3: Jump value becomes 2 (2^1 = 2). This is directly stair k. Call dfs(3, 0, 2).
        • Stair 3 is exactly stair k, hence a valid way. From here, still explore:
        • Up to Stair 7: Jump value becomes 3 (2^2 = 4). Call dfs(7, 0, 3).
          • Stair 7 exceeds stair k + 1, hence no valid paths.

In this walkthrough, two distinct paths are identified for Alice to reach stair 3:

  • Path 1: 1 -> 2 -> 1 -> 3
  • Path 2: 1 -> 2 -> 3

This example demonstrates the recursive exploration, and use of memoization prevents recalculating previously visited states, thus making the solution efficient. The function dfs(1, 0, 0) initiates this recursive exploration process.

Solution Implementation

1from functools import lru_cache
2
3class Solution:
4    def waysToReachStair(self, k: int) -> int:
5        # Use memoization to cache results of the dfs function
6        @lru_cache(None)
7        def dfs(i: int, j: int, jump: int) -> int:
8            # Base case: If the current position exceeds k + 1, return 0
9            if i > k + 1:
10                return 0
11            # Initialize the answer as 1 if the current position is exactly k, otherwise 0
12            ans = int(i == k)
13            # If not at the starting position (i > 0) and currently not in a jump (j = 0)
14            if i > 0 and j == 0:
15                # Explore the previous step and block further backward jumps
16                ans += dfs(i - 1, 1, jump)
17            # Explore the next position by incrementing based on the jump value
18            ans += dfs(i + (1 << jump), 0, jump + 1)
19            return ans
20
21        # Call the dfs starting from position 1, with no block and an initial jump of 0
22        return dfs(1, 0, 0)
23
1import java.util.HashMap;
2import java.util.Map;
3
4class Solution {
5
6    // Memoization map to store results for subproblems
7    private Map<Long, Integer> memoizationMap = new HashMap<>();
8
9    // Target staircase number
10    private int targetStair;
11
12    // Method to calculate the number of ways to reach the stair k
13    public int waysToReachStair(int k) {
14        // Initialize the target stair
15        this.targetStair = k;
16        // Start the depth-first search from the first stair with initial conditions
17        return dfs(1, 0, 0);
18    }
19
20    // Recursive DFS method to explore possible jumps
21    private int dfs(int currentStair, int backwardStep, int jumpSize) {
22        // If the current stair exceeds the target plus one, no solution is possible
23        if (currentStair > targetStair + 1) {
24            return 0;
25        }
26        // Construct a unique key for memoization based on current state
27        long key = ((long) currentStair << 32) | (jumpSize << 1) | backwardStep;
28      
29        // Check if the result is already computed and stored in the map
30        if (memoizationMap.containsKey(key)) {
31            return memoizationMap.get(key);
32        }
33
34        // Initialize the answer, counting if the current stair is the target
35        int ans = currentStair == targetStair ? 1 : 0;
36      
37        // If not at the starting point and not stepping back, explore going backward
38        if (currentStair > 0 && backwardStep == 0) {
39            ans += dfs(currentStair - 1, 1, jumpSize);
40        }
41      
42        // Explore jumping forward with increasing jump size
43        ans += dfs(currentStair + (1 << jumpSize), 0, jumpSize + 1);
44      
45        // Store the computed result in the memoization map
46        memoizationMap.put(key, ans);
47      
48        // Return the total number of ways found for this configuration
49        return ans;
50    }
51}
52
1#include <unordered_map>
2using namespace std;
3
4class Solution {
5public:
6    int waysToReachStair(int k) {
7        unordered_map<long long, int> memoizationMap; // Cache to store results of subproblems for memoization
8
9        // Define a lambda function for depth-first search with memoization
10        auto dfs = [&](auto&& dfs, int currentStair, int backStep, int jump) -> int {
11            // If current stair index exceeds k + 1, return 0 as it's an invalid path
12            if (currentStair > k + 1) {
13                return 0;
14            }
15
16            // Create a unique key for memoization from currentStair, backStep, and jump
17            long long key = ((long long)currentStair << 32) | (jump << 1) | backStep;
18
19            // Check if result has already been computed for the current state
20            if (memoizationMap.contains(key)) {
21                return memoizationMap[key];
22            }
23
24            // Initialize the number of ways for this state
25            int ways = (currentStair == k) ? 1 : 0;
26
27            // Evaluate potential paths by moving backward if backStep is 0
28            if (currentStair > 0 && backStep == 0) {
29                ways += dfs(dfs, currentStair - 1, 1, jump);
30            }
31
32            // Evaluate potential paths by moving forward and increasing jump length
33            ways += dfs(dfs, currentStair + (1 << jump), 0, jump + 1);
34
35            // Store computed result in memoization map
36            memoizationMap[key] = ways;
37
38            return ways;
39        };
40
41        // Start the depth-first search from stair index 1, with no back step and initial jump length
42        return dfs(dfs, 1, 0, 0);
43    }
44};
45
1/**
2 * Function to calculate the number of ways to reach the top of the stairs.
3 * @param k - The target stair to reach.
4 * @returns The number of ways to reach the specified stair.
5 */
6function waysToReachStair(k: number): number {
7    // Create a map to cache results of subproblems, used to avoid duplicate calculations.
8    const f: Map<bigint, number> = new Map();
9
10    /**
11     * Helper function using depth-first search with memoization.
12     * @param i - Current stair position being evaluated.
13     * @param j - Indicator for whether the last step was up (1) or not (0).
14     * @param jump - Current jump length used to calculate the next step.
15     * @returns The number of ways to reach the target stair from the current position.
16     */
17    const dfs = (i: number, j: number, jump: number): number => {
18        // Base case: If the current position exceeds the target + 1, return 0.
19        if (i > k + 1) {
20            return 0;
21        }
22
23        // Generate a unique key for the current state using bit manipulation.
24        const key: bigint = (BigInt(i) << BigInt(32)) | BigInt(jump << 1) | BigInt(j);
25      
26        // Check if the result for this state has already been calculated.
27        if (f.has(key)) {
28            return f.get(key)!;
29        }
30
31        let ans: number = 0;
32
33        // If the current position is exactly at the target, increment the result.
34        if (i === k) {
35            ans++;
36        }
37
38        // Explore the possibility of stepping back if last move was upwards.
39        if (i > 0 && j === 0) {
40            ans += dfs(i - 1, 1, jump);
41        }
42
43        // Explore the possibility of stepping forward with an increased jump length.
44        ans += dfs(i + (1 << jump), 0, jump + 1);
45
46        // Save the result for the current state to the cache.
47        f.set(key, ans);
48        return ans;
49    };
50
51    // Start the depth-first search from stair 1.
52    return dfs(1, 0, 0);
53}
54

Time and Space Complexity

The time complexity of the code is O(\log^2 k) because in the worst case scenario each recursive call to dfs may involve increasing the jump exponentially, leading to O(\log k) jumps. Each jump in turn could involve another level of recursion also requiring O(\log k) steps, resulting in an overall complexity of O(\log^2 k).

The space complexity is also O(\log^2 k). This complexity arises from the recursion stack and memoization caching. We maintain states of recursive calls in an amount similar to its time complexity, hence also O(\log^2 k).

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which data structure is used to implement recursion?


Recommended Readings

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


Load More