Facebook Pixel

2770. Maximum Number of Jumps to Reach the Last Index

Problem Description

You have an array nums with n integers (0-indexed) and an integer value target. You start at index 0 and want to reach the last index (n-1) by making jumps.

From any index i, you can jump to any index j if these conditions are met:

  • j must be ahead of i (meaning 0 <= i < j < n)
  • The difference between the values at these positions must be within the target range: -target <= nums[j] - nums[i] <= target

Your goal is to find the maximum number of jumps you can make to reach the last index. If it's impossible to reach the last index, return -1.

For example, if you're at index i, you can only jump to index j if j is ahead of you and the absolute difference |nums[j] - nums[i]| is at most target. You want to maximize the number of jumps taken while still being able to reach the end.

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

Intuition

The key insight is that we want to maximize the number of jumps, not minimize them. This means we should explore all possible valid paths from the starting position to the end and choose the one with the most jumps.

Think of this problem backwards: from any position i, we need to determine what's the maximum number of jumps we can make to reach the end. If we're already at the last index, we need 0 jumps. Otherwise, we look at all positions we can jump to from our current position and pick the path that gives us the most jumps.

This naturally leads to a recursive approach. For each position i, we:

  1. Check all positions j where j > i (positions ahead of us)
  2. Verify if we can jump there: |nums[j] - nums[i]| <= target
  3. If we can jump to j, calculate how many jumps we can make from j to the end
  4. Take the maximum among all valid jumps and add 1 (for the current jump)

The recursive nature of this problem makes it a perfect candidate for dynamic programming with memoization. Since we might visit the same position multiple times through different paths, we can cache the result for each position to avoid recalculating it.

The function dfs(i) represents "what's the maximum number of jumps from position i to reach the end?" If we can't reach the end from position i, we return negative infinity to indicate it's impossible. Starting from dfs(0) gives us the answer for the entire problem.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution implements a top-down dynamic programming approach using memoization with Python's @cache decorator.

Implementation Details:

  1. Main Function Setup: We define n = len(nums) to get the array length and call dfs(0) to start the recursion from index 0.

  2. Recursive Function dfs(i): This function calculates the maximum jumps from position i to reach index n-1.

    • Base Case: If i == n - 1, we've reached the destination, so return 0 (no more jumps needed)
    • Recursive Case: Initialize ans = -inf to track the maximum jumps. This negative infinity helps identify when no valid path exists.
  3. Exploring Valid Jumps: For each position i, we iterate through all possible jump destinations:

    for j in range(i + 1, n):
        if abs(nums[i] - nums[j]) <= target:
            ans = max(ans, 1 + dfs(j))
    • We check positions j from i+1 to n-1
    • For each valid jump (where |nums[i] - nums[j]| <= target), we calculate 1 + dfs(j)
    • The 1 accounts for the current jump from i to j
    • dfs(j) gives us the maximum jumps from j to the end
    • We keep the maximum value among all possible paths
  4. Memoization: The @cache decorator automatically stores results of dfs(i) for each unique i, preventing redundant calculations when the same position is reached through different paths.

  5. Final Result: After dfs(0) completes:

    • If ans < 0 (still negative infinity or negative), it means no valid path exists, so return -1
    • Otherwise, return ans as the maximum number of jumps

Time Complexity: O(n²) where each position is computed once (due to memoization) and for each position, we check up to n-1 possible jumps.

Space Complexity: O(n) for the memoization cache and recursion stack depth.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Example: nums = [1, 3, 6, 4, 1], target = 2

We want to find the maximum number of jumps from index 0 to index 4.

Step-by-step execution of dfs(0):

Starting at index 0 (value = 1):

  • Check index 1: |3 - 1| = 2 ≤ 2 ✓ Valid jump → Calculate 1 + dfs(1)
  • Check index 2: |6 - 1| = 5 > 2 ✗ Invalid jump
  • Check index 3: |4 - 1| = 3 > 2 ✗ Invalid jump
  • Check index 4: |1 - 1| = 0 ≤ 2 ✓ Valid jump → Calculate 1 + dfs(4)

Computing dfs(1) (value = 3):

  • Check index 2: |6 - 3| = 3 > 2 ✗ Invalid jump
  • Check index 3: |4 - 3| = 1 ≤ 2 ✓ Valid jump → Calculate 1 + dfs(3)
  • Check index 4: |1 - 3| = 2 ≤ 2 ✓ Valid jump → Calculate 1 + dfs(4)

Computing dfs(3) (value = 4):

  • Check index 4: |1 - 4| = 3 > 2 ✗ Invalid jump
  • No valid jumps → Return -inf

Computing dfs(4) (value = 1):

  • Base case: We're at the last index → Return 0

Backtracking with results:

  • dfs(4) = 0 (base case)
  • dfs(3) = -inf (no valid jumps to end)
  • dfs(1) chooses max between:
    • Path through index 3: 1 + (-inf) = -inf
    • Path through index 4: 1 + 0 = 1
    • Result: dfs(1) = 1
  • dfs(0) chooses max between:
    • Path through index 1: 1 + 1 = 2
    • Path through index 4: 1 + 0 = 1
    • Result: dfs(0) = 2

Final answer: 2 jumps (path: 0 → 1 → 4)

The algorithm explores all valid paths and selects the one with maximum jumps. The memoization ensures each position is computed only once, even if reached through multiple paths.

Solution Implementation

1class Solution:
2    def maximumJumps(self, nums: List[int], target: int) -> int:
3        from functools import cache
4        from math import inf
5      
6        @cache
7        def dfs(current_index: int) -> int:
8            """
9            Recursive function to find maximum jumps from current_index to the last index.
10          
11            Args:
12                current_index: The current position in the array
13              
14            Returns:
15                Maximum number of jumps from current_index to reach the last index,
16                or -inf if it's impossible
17            """
18            # Base case: reached the last index
19            if current_index == array_length - 1:
20                return 0
21          
22            # Initialize maximum jumps to negative infinity (impossible case)
23            max_jumps = -inf
24          
25            # Try jumping to every subsequent index
26            for next_index in range(current_index + 1, array_length):
27                # Check if jump is valid (difference between values is within target)
28                if abs(nums[current_index] - nums[next_index]) <= target:
29                    # Recursively calculate jumps from next_index and add 1 for current jump
30                    max_jumps = max(max_jumps, 1 + dfs(next_index))
31          
32            return max_jumps
33      
34        # Initialize array length
35        array_length = len(nums)
36      
37        # Start DFS from index 0
38        result = dfs(0)
39      
40        # Return -1 if no valid path exists, otherwise return the result
41        return -1 if result < 0 else result
42
1class Solution {
2    // Memoization array to store the maximum jumps from each index
3    private Integer[] memo;
4    // Input array of numbers
5    private int[] nums;
6    // Length of the input array
7    private int n;
8    // Maximum allowed difference between elements for a valid jump
9    private int target;
10
11    /**
12     * Finds the maximum number of jumps from index 0 to index n-1
13     * where each jump from index i to j is valid if |nums[i] - nums[j]| <= target
14     * 
15     * @param nums   The input array of integers
16     * @param target The maximum allowed difference for a valid jump
17     * @return The maximum number of jumps, or -1 if no path exists
18     */
19    public int maximumJumps(int[] nums, int target) {
20        // Initialize instance variables
21        this.n = nums.length;
22        this.target = target;
23        this.nums = nums;
24        this.memo = new Integer[n];
25      
26        // Calculate the maximum jumps starting from index 0
27        int result = dfs(0);
28      
29        // Return -1 if no valid path exists, otherwise return the result
30        return result < 0 ? -1 : result;
31    }
32
33    /**
34     * Depth-first search with memoization to find maximum jumps from index i to n-1
35     * 
36     * @param i The current index
37     * @return The maximum number of jumps from index i to n-1, or negative if impossible
38     */
39    private int dfs(int i) {
40        // Base case: reached the last index
41        if (i == n - 1) {
42            return 0;
43        }
44      
45        // Check if result is already computed
46        if (memo[i] != null) {
47            return memo[i];
48        }
49      
50        // Initialize with a very small value (representing impossible path)
51        int maxJumps = -(1 << 30);
52      
53        // Try jumping to all subsequent indices
54        for (int j = i + 1; j < n; j++) {
55            // Check if jump from i to j is valid (difference <= target)
56            if (Math.abs(nums[i] - nums[j]) <= target) {
57                // Recursively find maximum jumps from j and add 1 for current jump
58                maxJumps = Math.max(maxJumps, 1 + dfs(j));
59            }
60        }
61      
62        // Store and return the result
63        memo[i] = maxJumps;
64        return maxJumps;
65    }
66}
67
1class Solution {
2public:
3    int maximumJumps(vector<int>& nums, int target) {
4        int n = nums.size();
5      
6        // dp[i] stores the maximum number of jumps from index i to the last index
7        // -1 means not yet computed, negative values mean impossible to reach end
8        vector<int> dp(n, -1);
9      
10        // DFS with memoization to find maximum jumps from current index
11        function<int(int)> dfs = [&](int currentIndex) -> int {
12            // Base case: reached the last index
13            if (currentIndex == n - 1) {
14                return 0;
15            }
16          
17            // Return memoized result if already computed
18            if (dp[currentIndex] != -1) {
19                return dp[currentIndex];
20            }
21          
22            // Initialize with a large negative value (indicating impossible path)
23            dp[currentIndex] = INT_MIN / 2;
24          
25            // Try jumping to all valid positions from current index
26            for (int nextIndex = currentIndex + 1; nextIndex < n; ++nextIndex) {
27                // Check if jump is valid (absolute difference <= target)
28                if (abs(nums[currentIndex] - nums[nextIndex]) <= target) {
29                    // Update maximum jumps by taking this path
30                    dp[currentIndex] = max(dp[currentIndex], 1 + dfs(nextIndex));
31                }
32            }
33          
34            return dp[currentIndex];
35        };
36      
37        // Start DFS from index 0
38        int result = dfs(0);
39      
40        // Return -1 if no valid path exists, otherwise return the result
41        return result < 0 ? -1 : result;
42    }
43};
44
1/**
2 * Finds the maximum number of jumps to reach the last index
3 * @param nums - Array of integers representing positions
4 * @param target - Maximum allowed difference between consecutive jump positions
5 * @returns Maximum number of jumps to reach the last index, or -1 if impossible
6 */
7function maximumJumps(nums: number[], target: number): number {
8    const arrayLength: number = nums.length;
9    // Memoization array to store the maximum jumps from each index
10    // -1 indicates unvisited, negative values indicate impossible paths
11    const memo: number[] = Array(arrayLength).fill(-1);
12  
13    /**
14     * Depth-first search to find maximum jumps from current index
15     * @param currentIndex - Current position in the array
16     * @returns Maximum number of jumps from current index to the end
17     */
18    const findMaxJumps = (currentIndex: number): number => {
19        // Base case: reached the last index
20        if (currentIndex === arrayLength - 1) {
21            return 0;
22        }
23      
24        // Return memoized result if already computed
25        if (memo[currentIndex] !== -1) {
26            return memo[currentIndex];
27        }
28      
29        // Initialize with a very small number to indicate no valid path found yet
30        memo[currentIndex] = -(1 << 30);
31      
32        // Try jumping to all possible next positions
33        for (let nextIndex = currentIndex + 1; nextIndex < arrayLength; ++nextIndex) {
34            // Check if jump is valid (difference within target range)
35            if (Math.abs(nums[currentIndex] - nums[nextIndex]) <= target) {
36                // Update maximum jumps if this path yields more jumps
37                memo[currentIndex] = Math.max(
38                    memo[currentIndex], 
39                    1 + findMaxJumps(nextIndex)
40                );
41            }
42        }
43      
44        return memo[currentIndex];
45    };
46  
47    // Start DFS from index 0
48    const result: number = findMaxJumps(0);
49  
50    // Return -1 if no valid path exists, otherwise return the result
51    return result < 0 ? -1 : result;
52}
53

Time and Space Complexity

Time Complexity: O(n^2)

The solution uses dynamic programming with memoization. The dfs function can be called at most n times (once for each index from 0 to n-1). For each call to dfs(i), we iterate through all indices from i+1 to n-1 in the worst case, which takes O(n) time. Since we use memoization with @cache, each state i is computed only once. Therefore, the total time complexity is O(n) * O(n) = O(n^2).

Space Complexity: O(n)

The space complexity consists of two components:

  1. The recursion call stack can go up to depth n in the worst case (when we jump one index at a time from 0 to n-1), contributing O(n) space.
  2. The memoization cache stores at most n different states (one for each index), contributing O(n) space.

The total space complexity is O(n) + O(n) = O(n).

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

Common Pitfalls

1. Incorrect Base Case Handling

A common mistake is forgetting to handle the edge case where the input array has only one element. When nums has length 1, we're already at the last index (index 0), so no jumps are needed.

Incorrect approach:

def maximumJumps(self, nums: List[int], target: int) -> int:
    @cache
    def dfs(i):
        if i == n - 1:
            return 0
        # ... rest of code
  
    n = len(nums)
    result = dfs(0)
    return -1 if result < 0 else result

Issue: When len(nums) == 1, the function returns 0 instead of 0 jumps, which is actually correct but might be confusing in implementation.

Solution: Add explicit handling or ensure the base case is clear:

def maximumJumps(self, nums: List[int], target: int) -> int:
    n = len(nums)
    # Handle single element array explicitly
    if n == 1:
        return 0
  
    @cache
    def dfs(i):
        if i == n - 1:
            return 0
        # ... rest of code

2. Using Wrong Initial Value for Maximum Tracking

Another pitfall is initializing the maximum jumps variable incorrectly, such as using 0 or -1 instead of negative infinity.

Incorrect approach:

@cache
def dfs(i):
    if i == n - 1:
        return 0
  
    ans = -1  # Wrong! Should be -inf
    for j in range(i + 1, n):
        if abs(nums[i] - nums[j]) <= target:
            ans = max(ans, 1 + dfs(j))
    return ans

Issue: If no valid jumps exist from position i, returning -1 will propagate through the recursion. When we compute 1 + dfs(j) where dfs(j) = -1, we get 0, which incorrectly suggests a valid path exists.

Solution: Always use negative infinity for impossible cases:

from math import inf

@cache
def dfs(i):
    if i == n - 1:
        return 0
  
    ans = -inf  # Correct initialization
    for j in range(i + 1, n):
        if abs(nums[i] - nums[j]) <= target:
            ans = max(ans, 1 + dfs(j))
    return ans

3. Misunderstanding the Jump Condition

Some might incorrectly interpret the condition as requiring exact equality or using the wrong comparison.

Incorrect approaches:

# Wrong: Using wrong range check
if nums[j] - nums[i] <= target:  # Missing absolute value
    ans = max(ans, 1 + dfs(j))

# Wrong: Checking only positive difference
if 0 <= nums[j] - nums[i] <= target:  # Ignores negative differences
    ans = max(ans, 1 + dfs(j))

Solution: Always check the absolute difference:

if abs(nums[i] - nums[j]) <= target:
    ans = max(ans, 1 + dfs(j))

4. Memory Optimization Oversight

While @cache is convenient, in competitive programming or memory-constrained environments, it might cause issues with large inputs.

Alternative Solution using Bottom-Up DP:

def maximumJumps(self, nums: List[int], target: int) -> int:
    n = len(nums)
    dp = [-inf] * n
    dp[n-1] = 0
  
    for i in range(n-2, -1, -1):
        for j in range(i+1, n):
            if abs(nums[i] - nums[j]) <= target:
                dp[i] = max(dp[i], 1 + dp[j])
  
    return -1 if dp[0] < 0 else dp[0]

This approach uses O(n) space without recursion stack overhead and provides clearer memory usage patterns.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)


Recommended Readings

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

Load More