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
becomesjump + 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 stairk + 1
, it's impossible to reachk
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 calldfs
with the updated stair and parameters. -
Allow Alice to move upwards via
i + 2^jump
to explore other possible paths to reach stairk
.
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
exceedsk + 1
, return 0 since Alice cannot go back down consecutively to reachk
from beyondk + 1
.
- If
-
Target Reached:
- If
i
equalsk
, consider this a valid path, thus initializingans
to 1 and continue to explore other potential paths.
- If
-
Recursive Exploration:
- When
i > 0
and the previous operation was not a downward move (j == 0
), Alice can move downwards toi - 1
. We recursively calldfs(i - 1, 1, jump)
to explore this path. - For upward movement, compute the new stair position as
i + (1 << jump)
representing2^jump
steps up, and incrementjump
for subsequent operations. This is solved withdfs(i + (1 << jump), 0, jump + 1)
.
- When
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 EvaluatorExample 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:
- From Stair 1: Alice can move:
- Up to Stair 2: Jump value becomes 1 (
2^0 = 1
). Calldfs(2, 0, 1)
.
- Up to Stair 2: Jump value becomes 1 (
- From Stair 2:
- Up to Stair 4: Jump value becomes 2 (
2^1 = 2
). Calldfs(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.
- At this point, since stair 4 exceeds stair
- Down to Stair 1: Call
dfs(1, 1, 1)
.
- Up to Stair 4: Jump value becomes 2 (
- From Stair 1 with Downward Constraint:
- Up to Stair 3: Jump value becomes 2 (
2^1 = 2
). This is directly stairk
. Calldfs(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
). Calldfs(7, 0, 3)
.- Stair 7 exceeds stair
k + 1
, hence no valid paths.
- Stair 7 exceeds stair
- Stair 3 is exactly stair
- Up to Stair 3: Jump value becomes 2 (
- From Stair 1: Alice can move:
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.
Which data structure is used to implement recursion?
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!