2770. Maximum Number of Jumps to Reach the Last Index

Problem Description

The given problem presents us with a particular type of jumping puzzle. You have an array called nums which consists of n integers, indexed from 0 to n-1. You also have a value called target. Starting at the first element of the array (index 0), you can jump to any later element in the array (from index i to index j, where i < j) as long as the absolute difference between the values of nums[i] and nums[j] is less than or equal to target. The question is to determine the maximum number of jumps that can be made to reach the last element of the array (n-1 index).

If, at any point, there are no legal jumps to make, which means you cannot reach the last index of the array, the function should return -1.

In essence, the problem is asking for the furthest reach in the array through a series of legal jumps, where each jump abides by the rule concerning the allowable difference defined by the target value.


The solution approach relies on the idea of recursion and dynamic programming. We can define a recursive function, say dfs(i), that computes the maximum number of jumps needed to reach the end of the array starting from the current index i.

Initially, we might consider simply iterating through the array, and at each step, trying each possible legal jump. However, this brute-force approach can be highly inefficient as it involves many repeated calculations.

Instead, we can use a technique called memoization, which is a strategy to store the results of expensive function calls and return the cached result when the same inputs occur again. Thus, for each index i, we remember the maximum number of jumps we can make. If we revisit the same index i, we don't recalculate; we simply use the stored value.

Here's how the thought process goes for the given solution:

  1. If we are at the last index (n - 1), return 0 because we don't need any more jumps;
  2. If we are at any other index i, look at all potential jumps, i.e., loop from i + 1 to n - 1 and find indexes j to jump to where |nums[i] - nums[j]| <= target;
  3. Use our recursive function dfs(j) to compute the maximum number of jumps from index j to the end. The answer for our current position i would then be the maximum value of 1 + dfs(j) over all legal j's plus one (for the jump we are currently considering);
  4. If we cannot jump anywhere from i, we use negative infinity to mark that we can't reach the end from this index;
  5. We use memoization (@cache decorator) to avoid re-computation for indexes we have already visited.
  6. Finally, we initiate our recursive calls with dfs(0) to start the process from the first index. The result is either the maximum number of jumps or -1 if the end is unreachable (ans < 0).

This dynamic programming approach, combined with memoization (caching), provides an efficient solution to what could otherwise be a very time-consuming problem if solved with plain recursion or brute-forcing.

Learn more about Dynamic Programming patterns.

Solution Approach

The implementation uses a recursive depth-first search (DFS) approach in combination with memoization. The core of this approach is the dfs function, which solves the problem for a given index i and provides the maximum number of jumps that can be made from that index to the end. To prevent re-computation of results for each index i, memoization is used via the @cache decorator provided by Python's standard library. Here's an outline of how the algorithm works:

  • A helper function dfs(i) is defined:
    • If i is the last index (n - 1), return 0 because no more jumps are necessary.
    • If not, initialize a variable ans to negative infinity, symbolizing that the end is not yet reachable from i.
    • Loop through each potential target index j (where j goes from i + 1 to n - 1), and for each target index, check if the jump from i to j is legal; that is, if abs(nums[i] - nums[j]) <= target.
    • For every legal jump, use the dfs function to compute the maximum number of jumps from index j to the end. The value of ans is updated to be the maximum of the current ans and 1 + dfs(j), which represents making one jump to index j plus the maximum jumps from j to the end.
  • The global scope begins by determining n, the length of the nums array.
  • It then calls the helper function dfs(0) to initiate the recursive jumping process from the first element of the array.
  • Lastly, the function returns -1 if ans < 0 indicating that the last index is unreachable or ans if the end can be reached, providing the maximum number of jumps to reach the end.

Data structures used:

  • The nums array holds the input sequence of integers.
  • An implicit call stack for recursive function calls.
  • An internal cache for memoization, which is abstracted away by the @cache decorator but essentially behaves as a hash map storing computed results keyed by function arguments.

Algorithms and patterns used:

  • DFS (Depth-First Search): This recursive strategy is used to explore all possible jumping paths.
  • Memoization: This pattern avoids recalculating dfs(i) for any index by caching the result. When dfs(i) is called multiple times with the same i, it returns the cached result instead of recalculating.
  • Dynamic Programming (Top-Down Approach): The problem is broken down into smaller subproblems (dfs(j) for each j) and solved in a recursive manner. The overlapping subproblems are handled efficiently using memoization.

In summary, the solution employs a combination of recursive DFS and memoization in a top-down dynamic programming framework to efficiently compute the maximum number of jumps to the end of the array.

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

Which data structure is used to implement recursion?

Example Walkthrough

Let's illustrate the solution approach with a small example. Suppose we have the following input:

  • nums = [2, 3, 1, 1, 4]
  • target = 1

We want to find the maximum number of jumps to reach the end of the array.

  1. We call dfs(0), starting at index 0, where the value is 2. Our first action is to look for all indices we can jump to from 0.

  2. We can only jump to an index i if abs(nums[0] - nums[i]) <= target. In this case, we can jump from index 0 (nums[0] = 2) to index 1 (nums[1] = 3) because abs(2 - 3) = 1 <= target.

  3. Now at index 1, we call dfs(1) and look for a jump. We can jump from 1 to 2 and 1 to 3 since both satisfy the condition (abs(3 - 1) <= 1 and abs(3 - 1) <= 1).

  4. At index 2, calling dfs(2) returns 0, as we can jump directly to the end (index 4) from here (abs(1 - 4) <= 1).

  5. At index 3, calling dfs(3) also returns 0, for the same reason as dfs(2).

  6. Backtracking, we can see that from dfs(1), we have two possible destinations to consider: index 2 and index 3. Choosing index 2 lets us reach the end in one jump (the same for index 3), so we update our maximum jumps from 1 to 2.

  7. Finally, considering jumps from dfs(0) to dfs(1), we have now found that from dfs(1) we can reach the end with 2 jumps, hence we add one more jump (the jump from 0 to 1) and update our answer to 3.

  8. The recursive function will use memoization to ensure that if we call dfs(2) or dfs(3) again during exploration, it will return the cached result without recalculating it.

  9. If there were no possible jumps at any point, dfs(i) would return negative infinity to indicate that it's impossible to proceed. But in this case, we can reach the end, and the maximum number of jumps is 3.

Hence, dfs(0) will finally return 3 as the maximum number of jumps needed to reach the end of the array from the first element.

Using this approach, the solution capitalizes on the efficiency of memoization to avoid re-computing the same values, thereby reducing the time complexity significantly from what would be seen in a brute-force approach.

Solution Implementation

1from functools import lru_cache
2from math import inf
4class Solution:
5    def maximum_jumps(self, nums: List[int], target: int) -> int:
6        # This function seeks the maximum number of jumps
7        # satisfying the constraint where the absolute difference
8        # between the values at the current index and the chosen jump index
9        # is less than or equal to the target value.
11        # Cache the results of the dfs function to avoid recomputation
12        @lru_cache(maxsize=None)
13        def dfs(current_index: int) -> int:
14            # Base case: If we're at the last index, no more jumps are possible
15            if current_index == num_elements - 1:
16                return 0
18            # Initialize the maximum jumps from the current index to negative infinity
19            max_jumps_from_current = -inf
21            # Iterate over possible jumps from the current index
22            for next_index in range(current_index + 1, num_elements):
23                # If the jump is valid (difference within the target)
24                if abs(nums[current_index] - nums[next_index]) <= target:
25                    # Update max jumps from the current index by trying the jump
26                    # and adding 1 to the result of subsequent jumps
27                    max_jumps_from_current = max(max_jumps_from_current, 1 + dfs(next_index))
29            # Return the computed maximum jumps from the current index
30            return max_jumps_from_current
32        # Find out the number of elements in nums list
33        num_elements = len(nums)
35        # Start the depth-first search from the first index
36        max_jumps = dfs(0)
38        # If max jumps is negative, return -1 indicating no valid jumps sequence exists
39        # Otherwise, return the computed max jumps
40        return -1 if max_jumps < 0 else max_jumps
43Note that `lru_cache` from Python's `functools` is used to cache results, replacing the `@cache` decorator, which is not standard in Python 3 before version 3.9. Also, the variable `ans` is renamed to `max_jumps_from_current` to be more descriptive, and `n` is renamed to `num_elements` to better communicate its meaning.
45Import statements for `List` and `inf` should also be added:
48from typing import List  # Import List type for type hinting
1class Solution {
2    private Integer[] memo;  // Memoization array to store the maximum jumps from each position
3    private int[] nums;      // Array of numbers representing positions
4    private int n;           // Total number of positions
5    private int target;      // The maximum allowed difference between positions for a valid jump
7    // Method to calculate the maximum number of jumps to reach the end, starting from the first position
8    public int maximumJumps(int[] nums, int target) {
9        n = nums.length;           // Initialize the length of the nums array
10        this.target = target;      // Initialize the target difference
11        this.nums = nums;          // Assign the input array nums to the instance variable nums
12        memo = new Integer[n];     // Initialize the memoization array
13        int ans = dfs(0);          // Start a depth-first search from position 0
14        return ans < 0 ? -1 : ans; // If the result is negative, no valid sequence of jumps is possible, hence return -1; otherwise, return ans
15    }
17    // Helper method to perform a depth-first search, which calculates the maximum jumps from the current position 'i' to the end
18    private int dfs(int i) {
19        if (i == n - 1) {    // If the current position is the last one, no jumps are needed so return 0
20            return 0;
21        }
22        if (memo[i] != null) { // If we have already computed the number of jumps from this position, return it
23            return memo[i];
24        }
25        int ans = -(1 << 30);  // Initialize ans with a large negative number to ensure that any positive number will be greater during comparison
26        for (int j = i + 1; j < n; ++j) { // Explore all the positions that can be jumped to from position 'i'
27            if (Math.abs(nums[i] - nums[j]) <= target) { // If the difference between positions 'i' and 'j' is within the target range, explore position 'j'
28                ans = Math.max(ans, 1 + dfs(j)); // Update ans with the maximum jumps by considering the jump from 'i' to 'j' and the max jumps from 'j' to the end
29            }
30        }
31        return memo[i] = ans; // Memoize and return the maximum jumps from the current position 'i'
32    }
1#include <vector>
2#include <cstring>
3#include <functional>  // To use std::function
4using namespace std;
6class Solution {
8    int maximumJumps(vector<int>& nums, int target) {
9        int n = nums.size();
11        // f will store the maximum jumps from index i to the end
12        int max_jumps[n];
13        memset(max_jumps, -1, sizeof(max_jumps));  // Initialize with -1
15        // Depth-First Search (DFS) to calculate maximum jumps using memoization
16        function<int(int)> dfs = [&](int i) {
17            if (i == n - 1) {  // Base case: when we reach the last element
18                return 0;
19            }
20            if (max_jumps[i] != -1) {  // Check for already computed result
21                return max_jumps[i];
22            }
23            max_jumps[i] = INT_MIN;  // Initialize this state as minimum value
24            for (int j = i + 1; j < n; ++j) {  // Try to jump to every possible next step
25                if (abs(nums[i] - nums[j]) <= target) {  // Check if the jump is within the target difference
26                    max_jumps[i] = max(max_jumps[i], 1 + dfs(j));  // Recursively find the max jumps from the next index
27                }
28            }
29            return max_jumps[i];  // Return the maximum jumps from index i
30        };
32        // Start the process from index 0
33        int result = dfs(0);  
35        // If result is negative, it means it's not possible to jump to the end
36        return result < 0 ? -1 : result;
37    }
1// Define the maximumJumps function which calculates the maximum number of jumps needed to reach the last index
2function maximumJumps(nums: number[], target: number): number {
3    // Store the length of the nums array
4    const length = nums.length;
5    // Initialize an array to store the memoized results of subproblems
6    const memo: number[] = Array(length).fill(-1);
8    // Define the depth-first search (dfs) helper function
9    const dfs = (index: number): number => {
10        // If the current index is the last index of the array, no more jumps are needed
11        if (index === length - 1) {
12            return 0;
13        }
14        // If this index has been visited before, return the stored result
15        if (memo[index] !== -1) {
16            return memo[index];
17        }
18        // Set the current index's assumed minimum jumps to negative infinity
19        memo[index] = -(1 << 30);
20        // Iterate through the array starting from the current index plus one
21        for (let nextIndex = index + 1; nextIndex < length; ++nextIndex) {
22            // If the difference between the current index's value and the next index's value is within the target
23            if (Math.abs(nums[index] - nums[nextIndex]) <= target) {
24                // Update the current index's maximum jumps with the maximum of its current value and one plus the result of dfs at the next index
25                memo[index] = Math.max(memo[index], 1 + dfs(nextIndex));
26            }
27        }
28        // Return the computed number of jumps for the current index
29        return memo[index];
30    };
32    // Start the dfs from the first index to calculate the maximum number of jumps
33    const maxJumps = dfs(0);
34    // Return the result or -1 if it’s less than 0, indicating that the end is not reachable
35    return maxJumps < 0 ? -1 : maxJumps;

Time and Space Complexity

The given Python code defines a function maximumJumps that calculates the maximum number of jumps you can make in a list of integers, where a jump from index i to index j is valid if the absolute difference between nums[i] and nums[j] is less than or equal to a given target. The analysis of the time complexity and space complexity is as follows:

The time complexity of the maximumJumps function is O(n^2) where n is the length of the input array nums. This quadratic time complexity arises because, in the worst case, the recursive function dfs is called for every pair of indices i and j in the array. The outer loop runs once for each of the n elements, and the inner loop runs up to n-1 times for each iteration of the outer loop, hence the n * (n - 1) which simplifies to O(n^2).

The space complexity of the function is O(n) which is attributable to two factors:

  • The recursion depth can go up to n in the worst case, where each function call adds a new frame to the call stack.
  • The use of the @cache decorator on the dfs function adds memoization, storing the results of subproblems to prevent re-computation. As there are n possible starting positions for jumps, the cache could potentially hold n entries, one for each subproblem.

Therefore, the space used by the call stack and memoization dictates the space complexity of O(n).

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

Fast Track Your Learning with Our Quick Skills Quiz:

Which of these pictures shows the visit order of a depth-first search?

Recommended Readings

Got a question? Ask the Monster Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.