Find Number of Ways to Reach the K-th Stair
HardPrompt
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 + 2jump. And then,jumpbecomesjump + 1.
Return the total number of ways Alice can reach stair k.
Note that it is possible that Alice reaches the stair k, and performs some operations to reach the stair k again.
Example 1:
Input: k = 0
Output: 2
Explanation:
The 2 possible ways of reaching stair 0 are:
- Alice starts at stair 1.
- Using an operation of the first type, she goes down 1 stair to reach stair 0.
- Alice starts at stair 1.
- Using an operation of the first type, she goes down 1 stair to reach stair 0.
- Using an operation of the second type, she goes up 20 stairs to reach stair 1.
- Using an operation of the first type, she goes down 1 stair to reach stair 0.
Example 2:
Input: k = 1
Output: 4
Explanation:
The 4 possible ways of reaching stair 1 are:
- Alice starts at stair 1. Alice is at stair 1.
- Alice starts at stair 1.
- Using an operation of the first type, she goes down 1 stair to reach stair 0.
- Using an operation of the second type, she goes up 20 stairs to reach stair 1.
- Alice starts at stair 1.
- Using an operation of the second type, she goes up 20 stairs to reach stair 2.
- Using an operation of the first type, she goes down 1 stair to reach stair 1.
- Alice starts at stair 1.
- Using an operation of the first type, she goes down 1 stair to reach stair 0.
- Using an operation of the second type, she goes up 20 stairs to reach stair 1.
- Using an operation of the first type, she goes down 1 stair to reach stair 0.
- Using an operation of the second type, she goes up 21 stairs to reach stair 2.
- Using an operation of the first type, she goes down 1 stair to reach stair 1.
Constraints:
0 <= k <= 109
Approaches
3 approaches with complexity analysis and trade-offs.
A straightforward recursive approach that explores all possible paths. The state of the recursion is defined by (current_stair, current_jump, can_go_down). This method directly models the operations available to Alice and explores the state graph without any optimization.
Algorithm
- Define a recursive function, say
countWays(stair, jump, canGoDown). - The function explores two possible moves from the current state:
- Go Up: Move to
stair + 2^jump, incrementjumptojump + 1, and allow the next move to be a "go down" move. Recursively callcountWays(stair + 2^jump, jump + 1, true). - Go Down: If
stair > 0andcanGoDownis true, move tostair - 1. Thejumpvalue remains the same, and the next move cannot be a "go down" move. Recursively callcountWays(stair - 1, jump, false).
- Go Up: Move to
- The function's return value is the sum of ways from the subsequent recursive calls. A way is counted if the path eventually lands on stair
k. - The base case for the recursion is when the number of up-moves (
jump) or the currentstairexceeds a practical limit, beyond which it's impossible to reachk. Ifstair == k, we add 1 to our count for the current path. - The initial call would be
countWays(1, 0, true).
Walkthrough
This approach directly translates the problem statement into a recursive function. We define a state by (stair, jump, canGoDown). stair is the current stair, jump is the power for the next upward jump, and canGoDown is a boolean flag to enforce the rule that "go down" operations cannot be consecutive.
The function countWays(stair, jump, canGoDown) calculates the number of ways to reach k from this state. It sums the results from the recursive calls for the "go up" and "go down" moves. A crucial issue is defining the termination/base cases correctly to avoid infinite recursion and count paths properly. Due to the massive state space (k up to 10^9), this approach is not practical and will time out.
// This is a conceptual illustration. A direct implementation is not feasible// for the given constraints due to the massive state space and termination issues.public int waysToReachK_conceptual(int k) { // A practical implementation would need bounds for stair and jump to terminate. return solve(1, 0, true, k);} private int solve(long stair, int jump, boolean canGoDown, int k) { // Pruning is essential but difficult to get right. // If we are too far above k, it might be impossible to return. if (stair > k + 2 && (1L << jump) > stair - k) { // Heuristic pruning return 0; } if (jump > 35) { // Heuristic pruning return (stair == k) ? 1 : 0; } int count = 0; if (stair == k) { count = 1; } // Explore Go Up move count += solve(stair + (1L << jump), jump + 1, true, k); // Explore Go Down move if (canGoDown && stair > 0) { count += solve(stair - 1, jump, false, k); } return count;}Complexity
Time
Exponential, roughly O(2^N) where N is related to the number of moves. It explores a vast number of redundant paths and is too slow for the given constraints.
Space
O(D), where D is the maximum recursion depth. This can be very large.
Trade-offs
Pros
Simple to conceptualize as it directly follows the problem description.
Cons
Extremely inefficient and not feasible for the given constraints due to an enormous state space.
Prone to stack overflow for deep recursion paths.
Defining correct and effective termination/pruning conditions is very difficult.
This approach improves upon the brute-force recursion by using memoization to store and reuse the results for states that have already been computed. To make the state more manageable, we can define it relative to the target k, as (jump, diff, canGoDown), where diff = current_stair - k.
Algorithm
- Define a recursive function
solve(jump, diff, canGoDown)with a memoization table (e.g., a HashMap). - The state is defined by
jump(the next jump power),diff(the differencecurrent_stair - k), andcanGoDown(boolean flag). - The base case: if
diff == 0, we are at stairk, so we add 1 to our count. - Pruning: If
jumpis too large (e.g., > 32) ordiffmakes it impossible to reachk, return 0. For example, ifdiff > jump + 1, we need more "down" steps than available slots between "up" moves. - Check the memoization table. If the result for the current state
(jump, diff, canGoDown)is already computed, return it. - Recursively call for the two moves:
- Go Up:
solve(jump + 1, diff + 2^jump, true). - Go Down: If
canGoDownandk + diff > 0, callsolve(jump, diff - 1, false).
- Go Up:
- Store the sum of the results in the memoization table and return it.
- The initial call is
solve(0, 1 - k, true).
Walkthrough
This method refines the recursive approach by avoiding recomputation of the same subproblems using dynamic programming. We define a state relative to the target k to try and reduce the state space. The state is (jump, diff, canGoDown), where diff = current_stair - k.
The function solve(jump, diff, canGoDown) computes the number of ways to reach a state with diff = 0.
The state transitions are:
- Go Up: From
(jump, diff), we move to(jump + 1, diff + 2^jump). - Go Down: From
(jump, diff), we move to(jump, diff - 1). The main challenge remains the size of the state space fordiff.diffcan range from-kup to a positive value. Fork=10^9, this is too large. This approach is only practical for smallk.
// This approach is only feasible for small k.// For k <= 10^9, the 'diff' can be very large and negative, making the memoization map huge.class Solution { Map<String, Integer> memo; int targetK; public int waysToReachStair(int k) { this.targetK = k; // Using a HashMap for memoization. The key represents the state. this.memo = new HashMap<>(); return solve(1, 0, true); } private int solve(long stair, int jump, boolean canGoDown) { // Pruning: if we are too far, it's impossible to come back. // The number of down moves needed is stair - k. The max we can do is jump + 1. if (stair - targetK > jump + 1) { return 0; } // Further pruning if (jump > 31) { return (stair == targetK) ? 1 : 0; } String key = stair + "," + jump + "," + canGoDown; if (memo.containsKey(key)) { return memo.get(key); } int count = (stair == targetK) ? 1 : 0; // Go Up count += solve(stair + (1L << jump), jump + 1, true); // Go Down if (canGoDown && stair > 0) { count += solve(stair - 1, jump, false); } memo.put(key, count); return count; }}Complexity
Time
O(J * D), where J is O(log(k)) and D is O(k). The complexity is O(k * log(k)), which is too slow for the given constraints.
Space
O(J * D), where J is the max jump value (~32) and D is the range of `diff`. Since D can be O(k), this is O(log(k) * k), which is too large.
Trade-offs
Pros
Much faster than brute-force for smaller values of
k.Correctly applies the dynamic programming principle to avoid recomputing subproblems.
Cons
The state space for
diffis proportional tok, which can be up to 10^9. This makes the memoization table too large to be practical.Will cause Memory Limit Exceeded or Time Limit Exceeded for large
k.
This is the most efficient approach, which reframes the problem as a combinatorial selection problem. It analyzes the net effect of the moves and counts the valid arrangements of "go up" and "go down" operations, leading to a direct calculation without exploring the state space.
Algorithm
- Analyze the net displacement. If we perform
j"go up" moves, the total upward displacement is2^0 + 2^1 + ... + 2^(j-1) = 2^j - 1. Starting from stair 1, we would reach stair1 + (2^j - 1) = 2^j. - To reach stair
k, we must performd"go down" moves. The final position will be2^j - d. Setting this tokgivesd = 2^j - k. - Since
dmust be non-negative, we must have2^j >= k. - The "no consecutive down-moves" rule implies that a down-move must be followed by an up-move (unless it's the final move). This means we can place at most one down-move between any two consecutive up-moves, one before the first up-move, and one after the last up-move.
- This gives
j+1available "slots" to place theddown-moves. - The problem reduces to choosing
dslots out ofj+1. The number of ways is the binomial coefficientC(j+1, d). This also impliesd <= j+1. - Iterate through the number of up-moves
jfrom 0 upwards (up to ~31, as2^31is >10^9). - For each
j, calculated = 2^j - k. If0 <= d <= j+1, calculateC(j+1, d)and add it to the total count. - The loop can be optimized to stop when
2^j - j - 1 > k.
Walkthrough
This approach leverages a key insight: the jump value for the i-th "go up" move is always i-1, regardless of any "go down" moves made in between.
If we make exactly j "go up" moves, the total positive displacement is sum_{i=0}^{j-1} 2^i = 2^j - 1. Starting from stair 1, these moves alone would land us on stair 2^j. To end up on stair k, we need to make d = 2^j - k "go down" moves.
The number of down moves d must be non-negative, so 2^j >= k.
The rule against consecutive "go down" moves means each down-move must be separated by at least one up-move. This gives us j+1 potential slots to place the d down-moves: one before each of the j up-moves, and one after the last one.
_ U_0 _ U_1 _ ... _ U_{j-1} _
We need to choose d of these j+1 slots to place our down-moves. This is a combination problem, and the number of ways is C(j+1, d). This also implies that d cannot be greater than j+1.
So, the algorithm is to sum C(j+1, 2^j - k) for all j that satisfy 0 <= 2^j - k <= j+1. We can iterate j from 0 up to a small limit (around 31) and add the results.
class Solution { public int waysToReachStair(int k) { int ways = 0; // j is the number of 'up' moves. 2^30 > 10^9, so j won't exceed ~31. for (int j = 0; j < 32; j++) { long powerOf2 = 1L << j; long downs = powerOf2 - k; if (downs < 0) { continue; } // The number of available slots for down moves is j + 1. // One slot before all jumps, and one slot after each of the j jumps. if (downs > j + 1) { // For j > 1, 2^j - k > j + 1 is equivalent to 2^j - j - 1 > k. // Since 2^j - j - 1 is an increasing function for j>=1, // we can break the loop if this condition is met. if (j > 1 && (powerOf2 - j - 1 > k)) { break; } continue; } ways += combinations(j + 1, (int) downs); } return ways; } // Helper function to calculate combinations C(n, k) private int combinations(int n, int k) { if (k < 0 || k > n) { return 0; } if (k == 0 || k == n) { return 1; } if (k > n / 2) { k = n - k; } long res = 1; for (int i = 1; i <= k; i++) { res = res * (n - i + 1) / i; } return (int) res; }}Complexity
Time
O((log k)^2). The loop for `j` runs about `log k` times. Inside the loop, calculating combinations `C(j+1, d)` takes O(d) which is at most O(j). The total time is the sum of `j` from 0 to `log k`, which is O((log k)^2).
Space
O(1). We only use a few variables for the loop and calculations.
Trade-offs
Pros
Extremely efficient and runs in logarithmic time with respect to
k.Handles the large constraint on
kwith constant space.Provides an exact answer without exploring any paths.
Cons
Requires a mathematical insight into the problem structure which might not be immediately obvious.
Solutions
Solution
class Solution {private Map<Long, Integer> f = new HashMap<>();private int k;public int waysToReachStair(int k) { this.k = k; return dfs(1, 0, 0); }private int dfs(int i, int j, int jump) { if (i > k + 1) { return 0; } long key = ((long)i << 32) | jump << 1 | j; if (f.containsKey(key)) { return f.get(key); } int ans = i == k ? 1 : 0; if (i > 0 && j == 0) { ans += dfs(i - 1, 1, jump); } ans += dfs(i + (1 << jump), 0, jump + 1); f.put(key, ans); return ans; }}class Solution {public: int waysToReachStair(int k) { this->k = k; return dfs(1, 0, 0); } private: unordered_map<long long, int> f; int k; int dfs(int i, int j, int jump) { if (i > k + 1) { return 0; } long long key = ((long long)i << 32) | jump << 1 | j; if (f.contains(key)) { return f[key]; } int ans = i == k ? 1 : 0; if (i > 0 && j == 0) { ans += dfs(i - 1, 1, jump); } ans += dfs(i + (1 << jump), 0, jump + 1); f[key] = ans; return ans; }};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)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.