1340. Jump Game V
Problem Description
You are given an array of integers arr
and an integer d
representing the maximum jump distance. You need to find the maximum number of indices you can visit by jumping through the array.
From any index i
, you can jump to another index j
if the following conditions are met:
-
Distance constraint: The jump distance must be within
d
. This means:- You can jump forward to
i + x
where0 < x ≤ d
andi + x < arr.length
- You can jump backward to
i - x
where0 < x ≤ d
andi - x ≥ 0
- You can jump forward to
-
Height constraint: You can only jump from a higher value to a lower value. Specifically:
arr[i] > arr[j]
(the destination must have a smaller value)arr[i] > arr[k]
for all indicesk
betweeni
andj
(all intermediate values must also be smaller than the starting value)
You can start from any index in the array and begin jumping. Each index can be visited at most once in a single path. The goal is to find the maximum number of indices you can visit in a single sequence of jumps.
For example, if arr = [6,4,14,6,8,13,9,7,10,6,12]
and d = 2
:
- From index 2 (value 14), you could jump to index 1 (value 4) or index 3 (value 6) or index 4 (value 8), since they are within distance 2 and all have smaller values than 14
- From index 10 (value 12), you could jump to index 9 (value 6) since it's within distance 2 and has a smaller value
The solution uses dynamic programming with memoization to explore all possible jumping paths from each starting position and returns the maximum length path found.
Intuition
The key insight is that this problem has an optimal substructure property - if we know the maximum number of jumps we can make starting from each position, we can build up the solution for other positions.
Think of it this way: when standing at index i
, we need to find the longest path starting from this position. This path consists of:
- The current index (count = 1)
- Plus the longest path we can achieve from any valid jump destination
Since we can only jump to lower values (with all intermediate values also being lower), we're essentially creating a directed acyclic graph (DAG) where edges point from higher values to reachable lower values. This prevents cycles and makes the problem suitable for dynamic programming.
The recursive nature becomes clear: maxJumps(i) = 1 + max(maxJumps(j))
for all valid jump destinations j
from i
.
We use memoization (@cache
) because multiple starting positions might jump to the same intermediate position. For instance, if both index 2 and index 5 can jump to index 3, we don't want to recalculate the maximum jumps from index 3 twice.
The algorithm explores two directions from each position:
- Left direction: Check positions
i-1, i-2, ..., max(0, i-d)
and stop when we hit a value that's not smaller thanarr[i]
(violating the height constraint) - Right direction: Check positions
i+1, i+2, ..., min(n-1, i+d)
and stop when we hit a value that's not smaller thanarr[i]
The early stopping is crucial - once we encounter a value that's greater than or equal to arr[i]
, we know we can't jump over it to reach further positions, so we break out of that direction.
Finally, since we can start from any position, we try all possible starting points and return the maximum path length found.
Learn more about Dynamic Programming and Sorting patterns.
Solution Approach
The solution implements a top-down dynamic programming approach with memoization using depth-first search (DFS).
Core Implementation:
-
DFS Function with Memoization: The
dfs(i)
function calculates the maximum number of indices that can be visited starting from indexi
. The@cache
decorator automatically memoizes the results to avoid redundant calculations. -
Base Case: Each position can visit at least itself, so we initialize
ans = 1
. -
Exploring Left Jumps:
for j in range(i - 1, -1, -1): if i - j > d or arr[j] >= arr[i]: break ans = max(ans, 1 + dfs(j))
- Iterate backwards from
i-1
to0
- Check two stopping conditions:
- Distance exceeded:
i - j > d
- Height constraint violated:
arr[j] >= arr[i]
- Distance exceeded:
- If both conditions are satisfied, recursively calculate
dfs(j)
and update the maximum
- Iterate backwards from
-
Exploring Right Jumps:
for j in range(i + 1, n): if j - i > d or arr[j] >= arr[i]: break ans = max(ans, 1 + dfs(j))
- Iterate forwards from
i+1
ton-1
- Apply the same stopping conditions for distance and height
- Update the maximum with
1 + dfs(j)
if valid
- Iterate forwards from
-
Early Termination Optimization: The loops break immediately when encountering a value greater than or equal to
arr[i]
. This is valid because all subsequent positions in that direction would be unreachable (blocked by the taller or equal element). -
Global Maximum: Since we can start from any index, we compute
dfs(i)
for all positions and return the maximum:return max(dfs(i) for i in range(n))
Time Complexity: O(n * d)
where n
is the array length. Each position is computed once (due to memoization), and for each position, we check at most 2d
other positions.
Space Complexity: O(n)
for the memoization cache storing results for each index, plus O(n)
for the recursion stack in the worst case.
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 to illustrate the solution approach.
Example: arr = [7, 6, 5, 4, 3, 2]
, d = 2
Let's trace through the DFS calls starting from different positions:
Starting from index 0 (value 7):
- From index 0, we can jump within distance 2
- Check left: No positions to the left
- Check right:
- Index 1 (value 6): Valid! 6 < 7, distance = 1 ≤ 2
- Index 2 (value 5): Valid! 5 < 7, distance = 2 ≤ 2
- Now we recursively calculate:
dfs(1)
: From index 1 (value 6)- Can jump to index 2 (5) or index 3 (4)
dfs(2)
returns 3 (can visit 2→3→4)dfs(3)
returns 2 (can visit 3→4)- So
dfs(1) = 1 + max(3, 2) = 4
dfs(2)
: From index 2 (value 5)- Can jump to index 3 (4) or index 4 (3)
dfs(3)
returns 2 (already computed)dfs(4)
returns 1 (can only visit 4→5)- So
dfs(2) = 1 + max(2, 1) = 3
- Therefore,
dfs(0) = 1 + max(4, 3) = 5
Starting from index 3 (value 4):
- Check left:
- Index 2 (value 5): Can't jump (5 ≥ 4)
- Stop checking left
- Check right:
- Index 4 (value 3): Valid! 3 < 4
- Index 5 (value 2): Valid! 2 < 4
- Calculate:
dfs(4) = 2
(can visit 4→5)dfs(5) = 1
(no valid jumps)
- So
dfs(3) = 1 + max(2, 1) = 3
The memoization ensures we don't recalculate dfs(3)
, dfs(4)
, etc. when they're needed multiple times.
Final Result: The maximum path is 5, achieved by starting at index 0 and following the path: 0→1→2→3→4 (visiting indices with values 7→6→5→4→3).
This example demonstrates:
- How the algorithm explores both directions from each position
- The importance of the height constraint (can only jump to lower values)
- How memoization prevents redundant calculations
- The recursive nature of building up solutions from subproblems
Solution Implementation
1from typing import List
2from functools import cache
3
4class Solution:
5 def maxJumps(self, arr: List[int], d: int) -> int:
6 """
7 Find the maximum number of indices you can visit starting from any index.
8 You can jump from index i to index j if:
9 - i - d <= j <= i + d (j != i)
10 - arr[k] < arr[i] for all k between i and j (exclusive)
11
12 Args:
13 arr: List of integers representing heights
14 d: Maximum jump distance
15
16 Returns:
17 Maximum number of indices that can be visited
18 """
19 @cache
20 def dfs(index: int) -> int:
21 """
22 Perform depth-first search to find max jumps from given index.
23
24 Args:
25 index: Current position in the array
26
27 Returns:
28 Maximum number of indices reachable from current index
29 """
30 # Start with 1 (counting current index)
31 max_jumps = 1
32
33 # Try jumping to the left (indices before current)
34 for next_index in range(index - 1, -1, -1):
35 # Stop if distance exceeds d or we hit a taller/equal element
36 if index - next_index > d or arr[next_index] >= arr[index]:
37 break
38 # Update max jumps if jumping to next_index gives better result
39 max_jumps = max(max_jumps, 1 + dfs(next_index))
40
41 # Try jumping to the right (indices after current)
42 for next_index in range(index + 1, array_length):
43 # Stop if distance exceeds d or we hit a taller/equal element
44 if next_index - index > d or arr[next_index] >= arr[index]:
45 break
46 # Update max jumps if jumping to next_index gives better result
47 max_jumps = max(max_jumps, 1 + dfs(next_index))
48
49 return max_jumps
50
51 # Store array length for efficiency
52 array_length = len(arr)
53
54 # Find maximum jumps starting from each index
55 return max(dfs(i) for i in range(array_length))
56
1class Solution {
2 // Instance variables for array dimensions and memoization
3 private int arrayLength;
4 private int maxDistance;
5 private int[] heights;
6 private Integer[] memo; // Memoization array to store computed results
7
8 /**
9 * Finds the maximum number of indices you can visit in the array.
10 * You can jump from index i to index j if:
11 * 1. i != j
12 * 2. |i - j| <= d
13 * 3. All elements between i and j are strictly less than arr[i]
14 *
15 * @param arr Array of heights
16 * @param d Maximum jump distance
17 * @return Maximum number of indices that can be visited
18 */
19 public int maxJumps(int[] arr, int d) {
20 // Initialize instance variables
21 arrayLength = arr.length;
22 this.maxDistance = d;
23 this.heights = arr;
24 memo = new Integer[arrayLength];
25
26 int maxVisited = 1;
27
28 // Try starting from each index and find the maximum path
29 for (int startIndex = 0; startIndex < arrayLength; startIndex++) {
30 maxVisited = Math.max(maxVisited, dfs(startIndex));
31 }
32
33 return maxVisited;
34 }
35
36 /**
37 * Depth-first search to find the maximum number of jumps from index i.
38 * Uses memoization to avoid recomputation.
39 *
40 * @param currentIndex Current position in the array
41 * @return Maximum number of indices reachable from currentIndex (including itself)
42 */
43 private int dfs(int currentIndex) {
44 // Return cached result if already computed
45 if (memo[currentIndex] != null) {
46 return memo[currentIndex];
47 }
48
49 // Start with 1 (current position counts as visited)
50 int maxJumpsFromHere = 1;
51
52 // Try jumping to the left
53 for (int targetIndex = currentIndex - 1; targetIndex >= 0; targetIndex--) {
54 // Stop if distance exceeds limit or target is not lower
55 if (currentIndex - targetIndex > maxDistance || heights[targetIndex] >= heights[currentIndex]) {
56 break;
57 }
58 // Update maximum if jumping to targetIndex yields better result
59 maxJumpsFromHere = Math.max(maxJumpsFromHere, 1 + dfs(targetIndex));
60 }
61
62 // Try jumping to the right
63 for (int targetIndex = currentIndex + 1; targetIndex < arrayLength; targetIndex++) {
64 // Stop if distance exceeds limit or target is not lower
65 if (targetIndex - currentIndex > maxDistance || heights[targetIndex] >= heights[currentIndex]) {
66 break;
67 }
68 // Update maximum if jumping to targetIndex yields better result
69 maxJumpsFromHere = Math.max(maxJumpsFromHere, 1 + dfs(targetIndex));
70 }
71
72 // Cache and return the result
73 memo[currentIndex] = maxJumpsFromHere;
74 return maxJumpsFromHere;
75 }
76}
77
1class Solution {
2public:
3 int maxJumps(vector<int>& arr, int d) {
4 int n = arr.size();
5
6 // dp[i] stores the maximum number of indices that can be visited starting from index i
7 vector<int> dp(n, 0);
8
9 // DFS with memoization to calculate maximum jumps from index i
10 function<int(int)> dfs = [&](int index) -> int {
11 // Return cached result if already computed
12 if (dp[index] != 0) {
13 return dp[index];
14 }
15
16 // At minimum, we can visit the current index itself
17 int maxVisits = 1;
18
19 // Try jumping to the left (indices smaller than current)
20 for (int j = index - 1; j >= 0; --j) {
21 // Stop if distance exceeds d or we encounter a taller/equal building
22 if (index - j > d || arr[j] >= arr[index]) {
23 break;
24 }
25 // Update maximum visits if jumping to j gives better result
26 maxVisits = max(maxVisits, 1 + dfs(j));
27 }
28
29 // Try jumping to the right (indices larger than current)
30 for (int j = index + 1; j < n; ++j) {
31 // Stop if distance exceeds d or we encounter a taller/equal building
32 if (j - index > d || arr[j] >= arr[index]) {
33 break;
34 }
35 // Update maximum visits if jumping to j gives better result
36 maxVisits = max(maxVisits, 1 + dfs(j));
37 }
38
39 // Cache and return the result
40 dp[index] = maxVisits;
41 return maxVisits;
42 };
43
44 // Try starting from each index and find the maximum
45 int result = 1;
46 for (int i = 0; i < n; ++i) {
47 result = max(result, dfs(i));
48 }
49
50 return result;
51 }
52};
53
1/**
2 * Finds the maximum number of indices that can be visited starting from any index
3 * by jumping between array elements following specific rules
4 * @param arr - Array of building heights
5 * @param d - Maximum jump distance allowed
6 * @returns Maximum number of indices that can be visited
7 */
8function maxJumps(arr: number[], d: number): number {
9 const n = arr.length;
10
11 // dp[i] stores the maximum number of indices that can be visited starting from index i
12 const dp: number[] = new Array(n).fill(0);
13
14 /**
15 * DFS with memoization to calculate maximum jumps from a given index
16 * @param index - Starting index for the jump sequence
17 * @returns Maximum number of indices visitable from this index
18 */
19 const dfs = (index: number): number => {
20 // Return cached result if already computed
21 if (dp[index] !== 0) {
22 return dp[index];
23 }
24
25 // At minimum, we can visit the current index itself
26 let maxVisits = 1;
27
28 // Try jumping to the left (indices smaller than current)
29 for (let j = index - 1; j >= 0; j--) {
30 // Stop if distance exceeds d or we encounter a taller/equal building
31 if (index - j > d || arr[j] >= arr[index]) {
32 break;
33 }
34 // Update maximum visits if jumping to j gives better result
35 maxVisits = Math.max(maxVisits, 1 + dfs(j));
36 }
37
38 // Try jumping to the right (indices larger than current)
39 for (let j = index + 1; j < n; j++) {
40 // Stop if distance exceeds d or we encounter a taller/equal building
41 if (j - index > d || arr[j] >= arr[index]) {
42 break;
43 }
44 // Update maximum visits if jumping to j gives better result
45 maxVisits = Math.max(maxVisits, 1 + dfs(j));
46 }
47
48 // Cache and return the result
49 dp[index] = maxVisits;
50 return maxVisits;
51 };
52
53 // Try starting from each index and find the maximum
54 let result = 1;
55 for (let i = 0; i < n; i++) {
56 result = Math.max(result, dfs(i));
57 }
58
59 return result;
60}
61
Time and Space Complexity
Time Complexity: O(n * d)
The analysis breaks down as follows:
- The
dfs
function is called for each index in the array, which gives usn
unique calls due to memoization with@cache
- For each
dfs(i)
call, we explore up tod
positions to the left and up tod
positions to the right - In the worst case, we check
O(d)
positions in each direction before either hitting the distance limit or finding a taller/equal element - Since each state is computed only once due to caching, the total work is
O(n)
states ×O(d)
work per state =O(n * d)
Space Complexity: O(n)
The space usage consists of:
- The recursion stack depth can go up to
O(n)
in the worst case (when we can jump from index 0 to 1 to 2... to n-1 in a chain) - The
@cache
decorator stores up ton
different results, one for each index, usingO(n)
space - Total space complexity is
O(n)
for the cache plusO(n)
for the recursion stack =O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Height Constraint Check Between Indices
One of the most common mistakes is not properly checking that ALL intermediate values between the starting and destination indices are smaller than the starting value. Many developers incorrectly only check the destination value:
Incorrect Implementation:
# WRONG: Only checks destination, ignores intermediate values
for j in range(i - 1, max(i - d - 1, -1), -1):
if arr[j] < arr[i]: # Missing check for intermediate values!
ans = max(ans, 1 + dfs(j))
Why it's wrong: According to the problem, you can only jump from i
to j
if arr[k] < arr[i]
for ALL indices k
between i
and j
. The above code would incorrectly allow jumping over taller intermediate elements.
Correct Approach:
# CORRECT: Breaks immediately when hitting a taller/equal element
for j in range(i - 1, -1, -1):
if i - j > d or arr[j] >= arr[i]:
break # Can't jump past this point
ans = max(ans, 1 + dfs(j))
2. Off-by-One Errors in Distance Calculation
Another frequent mistake is incorrectly calculating the jump distance or using wrong comparison operators:
Incorrect Examples:
# WRONG: Uses >= instead of >
if i - j >= d or arr[j] >= arr[i]: # Should be i - j > d
break
# WRONG: Includes current index in distance calculation
for j in range(i - d, i): # Should ensure j != i and proper bounds
Correct Implementation:
The distance between indices should be strictly compared: i - j > d
(not >=
), and we must ensure we never jump to the same index.
3. Forgetting to Handle Edge Cases
Common Edge Case Mistakes:
- Not handling single-element arrays properly
- Incorrectly setting loop boundaries that could cause index out of bounds
- Not considering that you can start from ANY index (not just index 0)
Prevention: Always validate loop boundaries:
# Safe boundary checking
for j in range(i - 1, -1, -1): # Automatically stops at 0
if i - j > d: # Distance check prevents going too far
break
4. Missing or Incorrect Memoization
Without proper memoization, the solution becomes exponentially slow:
Incorrect:
def maxJumps(self, arr, d):
def dfs(i): # No memoization - will cause TLE!
# ... rest of logic
return max(dfs(i) for i in range(len(arr)))
Correct:
Use @cache
decorator or manual memoization to ensure each state is computed only once.
5. Breaking Too Early in the Loop
Some might think they can optimize by breaking when finding any valid jump:
Incorrect:
for j in range(i - 1, -1, -1):
if i - j > d or arr[j] >= arr[i]:
break
ans = max(ans, 1 + dfs(j))
break # WRONG: Stops after first valid jump, missing better options
Why it's wrong: You need to explore ALL valid jumps from each position to find the maximum path length. Breaking after the first valid jump would miss potentially longer paths.
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Don’t Miss This!