1269. Number of Ways to Stay in the Same Place After Some Steps
Problem Description
You start with a pointer at index 0
in an array of size arrLen
. At each step, you can perform one of three actions:
- Move the pointer 1 position to the left
- Move the pointer 1 position to the right
- Stay in the same position
The pointer must always remain within the array bounds (cannot go to negative indices or indices >= arrLen
).
Given two integers:
steps
: the exact number of steps you must takearrLen
: the size of the array
Your task is to find the number of different ways to move the pointer such that after exactly steps
steps, the pointer returns to index 0
.
Since the answer can be very large, return it modulo 10^9 + 7
.
For example, if steps = 3
and arrLen = 2
, you could:
- Stay at 0, stay at 0, stay at 0 (one way)
- Go right to 1, go left to 0, stay at 0 (another way)
- Stay at 0, go right to 1, go left to 0 (another way)
- Go right to 1, stay at 1, go left to 0 (another way)
The solution uses dynamic programming with memoization. The function dfs(i, j)
represents the number of ways to reach index 0
when currently at position i
with j
steps remaining. For each position and remaining steps, we consider all three possible moves (left, right, stay) and sum up the ways from those subsequent states.
Intuition
The key insight is that this is a path counting problem where we need to return to the starting position after a fixed number of steps. This naturally suggests a dynamic programming approach.
First, let's think about the constraints. With steps
limited to 500, even if we move right at every step, we can only reach position 500. This means we don't need to consider positions beyond min(arrLen, steps)
- any position further than steps
away from 0 is unreachable since we wouldn't have enough steps to get there and return.
The problem has optimal substructure: the number of ways to reach position 0 in j
steps from position i
depends on the number of ways to reach position 0 in j-1
steps from positions i-1
, i
, and i+1
(corresponding to moving right, staying, or moving left in the current step).
We can think of this recursively: if we're at position i
with j
steps remaining, we can:
- Move left to position
i-1
and solve forj-1
remaining steps - Stay at position
i
and solve forj-1
remaining steps - Move right to position
i+1
and solve forj-1
remaining steps
The base cases are:
- If we're at position 0 with 0 steps remaining, we've found one valid way (return 1)
- If we go out of bounds or run out of steps at a non-zero position, this path is invalid (return 0)
- If the position is farther from 0 than the remaining steps, it's impossible to return (return 0)
Since the same state (position, remaining_steps)
can be reached through different paths, we use memoization to avoid recalculating the same subproblems repeatedly. This transforms what would be an exponential time solution into a polynomial one.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization using Python's @cache
decorator.
Function Definition:
The core function dfs(i, j)
represents the number of ways to return to position 0 when currently at position i
with j
steps remaining.
Base Cases:
- Invalid states: If
i > j
(position is farther than remaining steps can reach), ori >= arrLen
(out of right boundary), ori < 0
(out of left boundary), orj < 0
(negative steps), return 0. - Success state: If
i == 0
andj == 0
, we've successfully returned to position 0 with exactly the required steps, return 1.
Recursive Transition:
For each valid state (i, j)
, we calculate the sum of ways from three possible moves:
ans = 0
for k in range(-1, 2): # k can be -1, 0, or 1
ans += dfs(i + k, j - 1)
ans %= mod
k = -1
: Move left to positioni - 1
k = 0
: Stay at positioni
k = 1
: Move right to positioni + 1
Each move consumes one step, so we recursively call with j - 1
remaining steps.
Modulo Operation:
Since the answer can be large, we apply modulo 10^9 + 7
after each addition to prevent integer overflow.
Memoization:
The @cache
decorator automatically memoizes the results, storing computed values for each unique (i, j)
pair. This prevents redundant calculations when the same state is reached through different paths.
Initial Call:
The solution starts with dfs(0, steps)
, meaning we begin at position 0 with all steps
available.
Time Complexity: O(steps × min(arrLen, steps))
as we compute each unique state at most once.
Space Complexity: O(steps × min(arrLen, steps))
for the memoization cache.
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 walk through a small example with steps = 4
and arrLen = 3
.
We need to find the number of ways to start at index 0, take exactly 4 steps, and return to index 0.
Starting with dfs(0, 4)
(at position 0 with 4 steps remaining):
Step 1: From dfs(0, 4) We can make three moves:
- Move left:
dfs(-1, 3)
→ returns 0 (out of bounds) - Stay:
dfs(0, 3)
→ need to compute - Move right:
dfs(1, 3)
→ need to compute
Step 2: Computing dfs(0, 3) From position 0 with 3 steps:
- Move left:
dfs(-1, 2)
→ returns 0 (out of bounds) - Stay:
dfs(0, 2)
→ need to compute - Move right:
dfs(1, 2)
→ need to compute
Step 3: Computing dfs(1, 3) From position 1 with 3 steps:
- Move left:
dfs(0, 2)
→ need to compute (or use memoized value) - Stay:
dfs(1, 2)
→ need to compute - Move right:
dfs(2, 2)
→ need to compute
Step 4: Computing dfs(0, 2) From position 0 with 2 steps:
- Move left:
dfs(-1, 1)
→ returns 0 (out of bounds) - Stay:
dfs(0, 1)
→ need to compute - Move right:
dfs(1, 1)
→ need to compute
Step 5: Computing dfs(1, 2) From position 1 with 2 steps:
- Move left:
dfs(0, 1)
→ need to compute (or use memoized value) - Stay:
dfs(1, 1)
→ need to compute - Move right:
dfs(2, 1)
→ returns 0 (position 2 > remaining steps 1, impossible to return)
Step 6: Computing base cases with 1 step remaining
dfs(0, 1)
: Can only stay →dfs(0, 0)
= 1 ✓dfs(1, 1)
: Can only move left →dfs(0, 0)
= 1 ✓
Step 7: Backtracking with computed values
dfs(0, 2)
= 0 + 1 + 1 = 2 (from steps: left=0, stay=1, right=1)dfs(1, 2)
= 1 + 1 + 0 = 2 (from steps: left=1, stay=1, right=0)dfs(2, 2)
= 1 + 0 + 0 = 1 (can only move left then left again)
Continuing this process:
dfs(0, 3)
= 0 + 2 + 2 = 4dfs(1, 3)
= 2 + 2 + 1 = 5
Finally:
dfs(0, 4)
= 0 + 4 + 5 = 9
So there are 9 different ways to return to position 0 after exactly 4 steps.
Some example paths:
- Stay → Stay → Stay → Stay
- Right → Left → Stay → Stay
- Right → Stay → Left → Stay
- Right → Right → Left → Left
- Stay → Right → Left → Stay (and 4 more...)
Solution Implementation
1class Solution:
2 def numWays(self, steps: int, arrLen: int) -> int:
3 """
4 Count the number of ways to stay at position 0 after 'steps' moves.
5 At each step, we can move left (-1), stay (0), or move right (+1).
6
7 Args:
8 steps: Total number of steps to take
9 arrLen: Length of the array (positions are 0 to arrLen-1)
10
11 Returns:
12 Number of ways to end at position 0, modulo 10^9 + 7
13 """
14 from functools import cache
15
16 MOD = 10**9 + 7
17
18 @cache
19 def dfs(position: int, remaining_steps: int) -> int:
20 """
21 Dynamic programming function to count valid paths.
22
23 Args:
24 position: Current position in the array
25 remaining_steps: Number of steps left to take
26
27 Returns:
28 Number of ways to reach position 0 with exactly 0 steps remaining
29 """
30 # Base case: out of bounds or negative steps
31 if position < 0 or position >= arrLen or remaining_steps < 0:
32 return 0
33
34 # Base case: too far from origin to return in remaining steps
35 if position > remaining_steps:
36 return 0
37
38 # Success case: at position 0 with no steps remaining
39 if position == 0 and remaining_steps == 0:
40 return 1
41
42 # Try all three possible moves: left, stay, right
43 ways = 0
44 for move in [-1, 0, 1]:
45 ways += dfs(position + move, remaining_steps - 1)
46 ways %= MOD
47
48 return ways
49
50 # Start at position 0 with 'steps' moves available
51 return dfs(0, steps)
52
1class Solution {
2 // Memoization table: memo[position][remainingSteps]
3 private Integer[][] memo;
4 // Array length
5 private int arrayLength;
6 // Modulo constant for large numbers
7 private static final int MOD = 1_000_000_007;
8
9 /**
10 * Counts the number of ways to stay at position 0 after exactly 'steps' moves.
11 * At each step, you can move left (-1), right (+1), or stay (0).
12 *
13 * @param steps The total number of steps to take
14 * @param arrLen The length of the array (bounds checking)
15 * @return The number of ways to end at position 0, modulo 10^9 + 7
16 */
17 public int numWays(int steps, int arrLen) {
18 // Initialize memoization table
19 // Maximum position we can reach is min(steps, arrLen-1)
20 memo = new Integer[steps][steps + 1];
21 arrayLength = arrLen;
22
23 // Start DFS from position 0 with all steps available
24 return dfs(0, steps);
25 }
26
27 /**
28 * Recursive helper function using DFS with memoization.
29 *
30 * @param position Current position in the array
31 * @param remainingSteps Number of steps remaining
32 * @return Number of ways to reach position 0 from current state
33 */
34 private int dfs(int position, int remainingSteps) {
35 // Base case: Invalid states
36 // - position > remainingSteps: Can't return to 0 in time
37 // - position >= arrayLength: Out of bounds (right)
38 // - position < 0: Out of bounds (left)
39 // - remainingSteps < 0: No steps left
40 if (position > remainingSteps || position >= arrayLength ||
41 position < 0 || remainingSteps < 0) {
42 return 0;
43 }
44
45 // Base case: Reached target position with no steps remaining
46 if (position == 0 && remainingSteps == 0) {
47 return 1;
48 }
49
50 // Check memoization table
51 if (memo[position][remainingSteps] != null) {
52 return memo[position][remainingSteps];
53 }
54
55 // Calculate the number of ways from current state
56 int totalWays = 0;
57
58 // Try all three possible moves: left (-1), stay (0), right (+1)
59 for (int move = -1; move <= 1; move++) {
60 totalWays = (totalWays + dfs(position + move, remainingSteps - 1)) % MOD;
61 }
62
63 // Store result in memoization table and return
64 memo[position][remainingSteps] = totalWays;
65 return totalWays;
66 }
67}
68
1class Solution {
2public:
3 int numWays(int steps, int arrLen) {
4 // dp[position][remainingSteps] = number of ways to reach position with remainingSteps
5 // Only need to consider positions up to 'steps' since we can't go further and return
6 int dp[steps][steps + 1];
7 memset(dp, -1, sizeof dp);
8
9 const int MOD = 1e9 + 7;
10
11 // DFS with memoization to count number of ways
12 // currentPos: current position in the array
13 // remainingSteps: number of steps remaining
14 function<int(int, int)> dfs = [&](int currentPos, int remainingSteps) -> int {
15 // Base cases: out of bounds or no steps remaining
16 if (currentPos > remainingSteps || currentPos >= arrLen || currentPos < 0 || remainingSteps < 0) {
17 return 0;
18 }
19
20 // Successfully reached position 0 with exactly 0 steps remaining
21 if (currentPos == 0 && remainingSteps == 0) {
22 return 1;
23 }
24
25 // Return memoized result if already computed
26 if (dp[currentPos][remainingSteps] != -1) {
27 return dp[currentPos][remainingSteps];
28 }
29
30 // Calculate number of ways by trying all three possible moves
31 int totalWays = 0;
32
33 // Three possible moves: left (-1), stay (0), right (+1)
34 for (int move = -1; move <= 1; ++move) {
35 totalWays = (totalWays + dfs(currentPos + move, remainingSteps - 1)) % MOD;
36 }
37
38 // Memoize and return the result
39 return dp[currentPos][remainingSteps] = totalWays;
40 };
41
42 // Start from position 0 with 'steps' remaining steps
43 return dfs(0, steps);
44 }
45};
46
1/**
2 * Calculate the number of ways to stay at position 0 after exactly 'steps' steps
3 * @param steps - Total number of steps to take
4 * @param arrLen - Length of the array (positions from 0 to arrLen-1)
5 * @returns Number of ways modulo 10^9 + 7
6 */
7function numWays(steps: number, arrLen: number): number {
8 // Memoization table: memo[position][remainingSteps] stores the number of ways
9 // Initialize with -1 to indicate uncomputed states
10 const memo: number[][] = Array.from(
11 { length: steps },
12 () => Array(steps + 1).fill(-1)
13 );
14
15 // Modulo value for large number handling
16 const MOD: number = 10 ** 9 + 7;
17
18 /**
19 * DFS helper function to calculate number of ways recursively
20 * @param currentPosition - Current position in the array
21 * @param remainingSteps - Number of steps remaining
22 * @returns Number of ways to reach position 0 with 0 steps remaining
23 */
24 const dfs = (currentPosition: number, remainingSteps: number): number => {
25 // Base case: Invalid positions or negative steps
26 if (currentPosition > remainingSteps ||
27 currentPosition >= arrLen ||
28 currentPosition < 0 ||
29 remainingSteps < 0) {
30 return 0;
31 }
32
33 // Base case: Reached target (position 0 with 0 steps remaining)
34 if (currentPosition === 0 && remainingSteps === 0) {
35 return 1;
36 }
37
38 // Return memoized result if already computed
39 if (memo[currentPosition][remainingSteps] !== -1) {
40 return memo[currentPosition][remainingSteps];
41 }
42
43 // Calculate total ways by trying all three possible moves
44 let totalWays: number = 0;
45
46 // Try three moves: left (-1), stay (0), right (+1)
47 for (let move = -1; move <= 1; move++) {
48 totalWays = (totalWays + dfs(currentPosition + move, remainingSteps - 1)) % MOD;
49 }
50
51 // Memoize and return the result
52 memo[currentPosition][remainingSteps] = totalWays;
53 return totalWays;
54 };
55
56 // Start DFS from position 0 with all steps available
57 return dfs(0, steps);
58}
59
Time and Space Complexity
The time complexity is O(steps × min(steps, arrLen))
, and the space complexity is O(steps × min(steps, arrLen))
.
The analysis can be refined to O(steps²)
for both time and space complexity when considering the practical constraints. Here's why:
For the recursive function dfs(i, j)
:
- Parameter
j
ranges from0
tosteps
- Parameter
i
represents the position in the array, ranging from0
toarrLen - 1
- However, due to the nature of the problem, we can only move at most
steps
distance from the starting position0
, so the effective range ofi
is bounded bymin(steps, arrLen - 1)
Since we start at position 0
and have steps
moves, with each move allowing us to go left, right, or stay, the maximum distance we can reach from origin is steps
. Therefore:
- The number of unique states is
O(steps × min(steps, arrLen))
- When
arrLen > steps
, the effective positions we can reach are limited to[0, steps]
, making itO(steps²)
- When
arrLen ≤ steps
, we haveO(steps × arrLen)
states
The reference answer simplifies this to O(steps × steps)
because in most practical scenarios, the positions we can actually reach are limited by the number of steps, effectively making the complexity O(steps²)
for both time and space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Optimizing the Search Space
A critical optimization that's easy to miss is that we don't need to consider positions beyond min(arrLen, steps // 2 + 1)
. Since we start at position 0 and must return to 0, the maximum distance we can travel is steps // 2
(go out and come back). Many solutions fail to include this optimization, leading to unnecessary computation and potential timeout for large inputs.
Problem Code:
@cache
def dfs(position: int, remaining_steps: int) -> int:
if position < 0 or position >= arrLen or remaining_steps < 0:
return 0
# ... rest of the code
Solution:
# Optimize the array length to avoid unnecessary states
max_position = min(arrLen, steps // 2 + 1)
@cache
def dfs(position: int, remaining_steps: int) -> int:
if position < 0 or position >= max_position or remaining_steps < 0:
return 0
# ... rest of the code
2. Forgetting to Apply Modulo Consistently
Another common mistake is forgetting to apply the modulo operation after accumulating results, which can cause integer overflow in languages with fixed integer sizes or incorrect results.
Problem Code:
ways = 0 for move in [-1, 0, 1]: ways += dfs(position + move, remaining_steps - 1) return ways # Forgot to apply modulo!
Solution:
ways = 0 for move in [-1, 0, 1]: ways += dfs(position + move, remaining_steps - 1) ways %= MOD # Apply modulo after each addition return ways
3. Incorrect Base Case Ordering
Checking base cases in the wrong order can lead to incorrect results. The "too far to return" check (position > remaining_steps
) should come after boundary checks to avoid index errors.
Problem Code:
def dfs(position: int, remaining_steps: int) -> int:
# Checking distance before boundaries - could cause issues
if position > remaining_steps:
return 0
if position < 0 or position >= arrLen:
return 0
Solution:
def dfs(position: int, remaining_steps: int) -> int:
# Check boundaries first
if position < 0 or position >= arrLen or remaining_steps < 0:
return 0
# Then check if too far to return
if position > remaining_steps:
return 0
4. Memory Limit Issues with Large Inputs
For very large values of steps
and arrLen
, the memoization cache can grow too large. Using the optimization from pitfall #1 is crucial to prevent this.
Complete Optimized Solution:
class Solution:
def numWays(self, steps: int, arrLen: int) -> int:
from functools import cache
MOD = 10**9 + 7
# Key optimization: limit search space
max_position = min(arrLen, steps // 2 + 1)
@cache
def dfs(position: int, remaining_steps: int) -> int:
# Boundary and validity checks
if position < 0 or position >= max_position or remaining_steps < 0:
return 0
# Optimization: impossible to return to 0
if position > remaining_steps:
return 0
# Success case
if position == 0 and remaining_steps == 0:
return 1
# Try all three moves
ways = 0
for move in [-1, 0, 1]:
ways += dfs(position + move, remaining_steps - 1)
ways %= MOD
return ways
return dfs(0, steps)
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!