1696. Jump Game VI
Problem Description
You have an array of integers nums
and a maximum jump distance k
. You start at index 0 and want to reach the last index of the array.
From any position i
, you can jump forward to any position between i + 1
and min(n - 1, i + k)
. In other words, you can jump at most k
steps forward, but you cannot jump outside the array boundaries.
As you jump through the array, you collect points. Your score is the sum of all values nums[j]
at each index j
that you land on during your journey (including the starting position at index 0 and the final position at index n - 1
).
Your goal is to find the maximum possible score you can achieve by choosing the optimal jumping path from index 0 to index n - 1
.
For example, if nums = [1, -1, -2, 4, -7, 3]
and k = 2
:
- You start at index 0 (score = 1)
- You could jump to index 1 or 2
- From there, continue jumping with the constraint of at most 2 steps forward
- You must eventually reach index 5
- The sum of all
nums[j]
values at positions you visit becomes your total score
The challenge is to find the jumping sequence that maximizes this total score.
Intuition
To find the maximum score, we need to think about this problem backwards. At each position i
, we want to know: "What's the maximum score I can achieve to reach this position?"
Let's define f[i]
as the maximum score when reaching index i
. To reach position i
, we must have come from some previous position j
where j
is within jumping distance (meaning i - k ≤ j ≤ i - 1
). The score at position i
would be the score at position j
plus the value at position i
.
This gives us the recurrence relation: f[i] = max(f[j]) + nums[i]
for all valid j
in range [i - k, i - 1]
.
The straightforward approach would be to check all possible previous positions for each index, but this would be inefficient with time complexity O(n * k)
.
The key optimization insight is that we're repeatedly finding the maximum value in a sliding window of size k
. As we move from index i
to i + 1
, the window of valid previous positions slides forward by one position. This is a classic sliding window maximum problem.
We can maintain a monotonic deque to efficiently track the maximum value in the current window. The deque stores indices, and we maintain the property that the f
values corresponding to these indices are in decreasing order. This way:
- The front of the deque always contains the index with the maximum
f
value in the current window - When adding a new index, we remove all indices from the back that have smaller or equal
f
values (they'll never be useful) - We remove indices from the front that are now outside the valid window (distance > k)
This optimization reduces the time complexity to O(n)
since each index is added and removed from the deque at most once.
Learn more about Queue, Dynamic Programming, Monotonic Queue and Heap (Priority Queue) patterns.
Solution Approach
The solution implements dynamic programming with monotonic queue optimization. Let's walk through the implementation step by step:
1. Initialize the DP array and monotonic deque:
f = [0] * n # f[i] stores maximum score to reach index i q = deque([0]) # monotonic deque storing indices
The deque initially contains index 0 since we start there.
2. Process each position from left to right:
For each index i
from 0 to n-1
:
3. Remove outdated indices from the deque front:
if i - q[0] > k: q.popleft()
If the index at the front of the deque is more than k
steps away from current position i
, it's no longer reachable in one jump, so we remove it.
4. Calculate the maximum score for position i:
f[i] = nums[i] + f[q[0]]
The front of the deque (q[0]
) contains the index with the maximum f
value among all valid previous positions. We add nums[i]
to this maximum to get f[i]
.
5. Maintain the monotonic property of the deque:
while q and f[q[-1]] <= f[i]: q.pop()
Before adding index i
to the deque, we remove all indices from the back whose f
values are less than or equal to f[i]
. These indices will never be optimal choices in the future because:
- Index
i
has a better (or equal) score - Index
i
is more recent, so it will remain valid for longer
6. Add current index to the deque:
q.append(i)
7. Return the final result:
return f[-1]
The maximum score to reach the last index is stored in f[n-1]
.
Time Complexity: O(n)
- Each index is added to and removed from the deque at most once.
Space Complexity: O(n)
for the DP array, and O(k)
for the deque (at most k+1 elements).
The monotonic deque ensures we can always find the maximum f
value in the valid window in O(1)
time by checking the front element, making this solution highly efficient.
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 nums = [10, -5, -2, 4, 0, 3]
and k = 3
.
We want to find the maximum score jumping from index 0 to index 5, where we can jump at most 3 steps forward from any position.
Initial Setup:
f = [0, 0, 0, 0, 0, 0]
(will store max scores)q = deque([0])
(monotonic deque with indices)
Step 1: Process index 0
- No indices to remove (starting position)
f[0] = nums[0] + 0 = 10
q = deque([0])
Step 2: Process index 1
- Check if front index too far:
1 - 0 = 1 ≤ 3
✓ (keep it) f[1] = nums[1] + f[0] = -5 + 10 = 5
- Compare with back of deque:
f[0] = 10 > f[1] = 5
, so keep index 0 q = deque([0, 1])
Step 3: Process index 2
- Check if front index too far:
2 - 0 = 2 ≤ 3
✓ (keep it) f[2] = nums[2] + f[0] = -2 + 10 = 8
(using max from front of deque)- Compare with back:
f[1] = 5 < f[2] = 8
, remove index 1 - Compare with back:
f[0] = 10 > f[2] = 8
, keep index 0 q = deque([0, 2])
Step 4: Process index 3
- Check if front index too far:
3 - 0 = 3 ≤ 3
✓ (keep it) f[3] = nums[3] + f[0] = 4 + 10 = 14
- Compare with back:
f[2] = 8 < f[3] = 14
, remove index 2 - Compare with back:
f[0] = 10 < f[3] = 14
, remove index 0 q = deque([3])
Step 5: Process index 4
- Check if front index too far:
4 - 3 = 1 ≤ 3
✓ (keep it) f[4] = nums[4] + f[3] = 0 + 14 = 14
- Compare with back:
f[3] = 14 = f[4] = 14
, remove index 3 q = deque([4])
Step 6: Process index 5
- Check if front index too far:
5 - 4 = 1 ≤ 3
✓ (keep it) f[5] = nums[5] + f[4] = 3 + 14 = 17
- Final answer:
f[5] = 17
Optimal Path: 0 → 3 → 5
- Start at index 0: collect 10
- Jump to index 3: collect 4
- Jump to index 5: collect 3
- Total score: 10 + 4 + 3 = 17
The monotonic deque efficiently maintained the maximum reachable score at each step, allowing us to compute the optimal solution in linear time.
Solution Implementation
1class Solution:
2 def maxResult(self, nums: List[int], k: int) -> int:
3 """
4 Find the maximum score when jumping through the array.
5 You can jump at most k steps forward from your current position.
6
7 Args:
8 nums: Array of integers representing scores at each position
9 k: Maximum jump distance
10
11 Returns:
12 Maximum score achievable when reaching the last index
13 """
14 n = len(nums)
15
16 # dp[i] represents the maximum score when reaching index i
17 dp = [0] * n
18
19 # Monotonic deque to maintain indices of potential maximum values
20 # within the sliding window of size k
21 deque_indices = deque([0])
22
23 for i in range(n):
24 # Remove indices that are outside the valid jump range (window size k)
25 if i - deque_indices[0] > k:
26 deque_indices.popleft()
27
28 # Calculate maximum score for current position:
29 # current value + best score from a reachable previous position
30 dp[i] = nums[i] + dp[deque_indices[0]]
31
32 # Maintain monotonic decreasing property of the deque
33 # Remove indices with smaller or equal dp values from the back
34 while deque_indices and dp[deque_indices[-1]] <= dp[i]:
35 deque_indices.pop()
36
37 # Add current index to the deque
38 deque_indices.append(i)
39
40 # Return the maximum score when reaching the last position
41 return dp[-1]
42
1class Solution {
2 public int maxResult(int[] nums, int k) {
3 int n = nums.length;
4 // dp[i] represents the maximum score to reach index i
5 int[] dp = new int[n];
6
7 // Monotonic deque to maintain indices of potential maximum values
8 // within the sliding window of size k
9 Deque<Integer> deque = new ArrayDeque<>();
10
11 // Initialize with index 0
12 deque.offer(0);
13 dp[0] = nums[0];
14
15 for (int i = 1; i < n; i++) {
16 // Remove indices that are outside the window of size k
17 // The valid range is [i-k, i-1]
18 while (!deque.isEmpty() && deque.peekFirst() < i - k) {
19 deque.pollFirst();
20 }
21
22 // Calculate maximum score to reach current index i
23 // It equals current value plus the maximum score from valid previous positions
24 dp[i] = nums[i] + dp[deque.peekFirst()];
25
26 // Maintain monotonic property of the deque
27 // Remove indices with smaller or equal dp values from the back
28 // since they won't be useful for future calculations
29 while (!deque.isEmpty() && dp[deque.peekLast()] <= dp[i]) {
30 deque.pollLast();
31 }
32
33 // Add current index to the deque
34 deque.offerLast(i);
35 }
36
37 // Return the maximum score to reach the last index
38 return dp[n - 1];
39 }
40}
41
1class Solution {
2public:
3 int maxResult(vector<int>& nums, int k) {
4 int n = nums.size();
5
6 // dp[i] represents the maximum score to reach index i
7 vector<int> dp(n);
8 dp[0] = nums[0]; // Base case: starting position score
9
10 // Monotonic deque to maintain indices of potential maximum values
11 // within the sliding window of size k
12 deque<int> dq;
13 dq.push_back(0); // Initialize with the first index
14
15 // Process each position starting from index 1
16 for (int i = 1; i < n; ++i) {
17 // Remove indices that are outside the valid jump range (window size k)
18 while (!dq.empty() && i - dq.front() > k) {
19 dq.pop_front();
20 }
21
22 // Calculate maximum score to reach current position i
23 // by jumping from the best position within range
24 dp[i] = nums[i] + dp[dq.front()];
25
26 // Maintain monotonic decreasing property of the deque
27 // Remove indices with smaller or equal dp values from the back
28 while (!dq.empty() && dp[i] >= dp[dq.back()]) {
29 dq.pop_back();
30 }
31
32 // Add current index to the deque
33 dq.push_back(i);
34 }
35
36 // Return the maximum score to reach the last index
37 return dp[n - 1];
38 }
39};
40
1/**
2 * Finds the maximum score you can obtain from jumping through the array
3 * where you can jump at most k steps forward from your current position
4 * @param nums - Array of integers representing scores at each position
5 * @param k - Maximum number of steps you can jump forward
6 * @returns Maximum score achievable
7 */
8function maxResult(nums: number[], k: number): number {
9 const n = nums.length;
10 // dp[i] represents the maximum score to reach position i
11 const dp: number[] = Array(n).fill(0);
12 // Deque to maintain indices in decreasing order of their dp values
13 // This helps us efficiently find the maximum dp value within the jump range
14 const deque = new Deque<number>();
15
16 // Initialize with starting position
17 deque.pushBack(0);
18 dp[0] = nums[0];
19
20 for (let i = 1; i < n; ++i) {
21 // Remove indices that are out of jump range (more than k steps away)
22 while (!deque.isEmpty() && i - deque.frontValue()! > k) {
23 deque.popFront();
24 }
25
26 // Calculate maximum score for current position
27 // Best previous position is at the front of deque
28 dp[i] = nums[i] + dp[deque.frontValue()!];
29
30 // Maintain deque in decreasing order of dp values
31 // Remove elements from back that have smaller or equal dp values
32 while (!deque.isEmpty() && dp[i] >= dp[deque.backValue()!]) {
33 deque.popBack();
34 }
35
36 // Add current index to deque
37 deque.pushBack(i);
38 }
39
40 return dp[n - 1];
41}
42
43/**
44 * Node class for doubly linked list implementation
45 */
46class Node<T> {
47 value: T;
48 next: Node<T> | null;
49 prev: Node<T> | null;
50
51 constructor(value: T) {
52 this.value = value;
53 this.next = null;
54 this.prev = null;
55 }
56}
57
58/**
59 * Double-ended queue (Deque) implementation using doubly linked list
60 * Supports O(1) insertion and deletion from both ends
61 */
62class Deque<T> {
63 private front: Node<T> | null;
64 private back: Node<T> | null;
65 private size: number;
66
67 constructor() {
68 this.front = null;
69 this.back = null;
70 this.size = 0;
71 }
72
73 /**
74 * Adds an element to the front of the deque
75 * @param val - Value to be added
76 */
77 pushFront(val: T): void {
78 const newNode = new Node(val);
79
80 if (this.isEmpty()) {
81 // First element in deque
82 this.front = newNode;
83 this.back = newNode;
84 } else {
85 // Link new node to current front
86 newNode.next = this.front;
87 this.front!.prev = newNode;
88 this.front = newNode;
89 }
90 this.size++;
91 }
92
93 /**
94 * Adds an element to the back of the deque
95 * @param val - Value to be added
96 */
97 pushBack(val: T): void {
98 const newNode = new Node(val);
99
100 if (this.isEmpty()) {
101 // First element in deque
102 this.front = newNode;
103 this.back = newNode;
104 } else {
105 // Link new node to current back
106 newNode.prev = this.back;
107 this.back!.next = newNode;
108 this.back = newNode;
109 }
110 this.size++;
111 }
112
113 /**
114 * Removes and returns the element from the front of the deque
115 * @returns The removed element or undefined if deque is empty
116 */
117 popFront(): T | undefined {
118 if (this.isEmpty()) {
119 return undefined;
120 }
121
122 const value = this.front!.value;
123 this.front = this.front!.next;
124
125 if (this.front !== null) {
126 // Disconnect from removed node
127 this.front.prev = null;
128 } else {
129 // Deque is now empty
130 this.back = null;
131 }
132
133 this.size--;
134 return value;
135 }
136
137 /**
138 * Removes and returns the element from the back of the deque
139 * @returns The removed element or undefined if deque is empty
140 */
141 popBack(): T | undefined {
142 if (this.isEmpty()) {
143 return undefined;
144 }
145
146 const value = this.back!.value;
147 this.back = this.back!.prev;
148
149 if (this.back !== null) {
150 // Disconnect from removed node
151 this.back.next = null;
152 } else {
153 // Deque is now empty
154 this.front = null;
155 }
156
157 this.size--;
158 return value;
159 }
160
161 /**
162 * Returns the value at the front without removing it
163 * @returns The front element or undefined if deque is empty
164 */
165 frontValue(): T | undefined {
166 return this.front?.value;
167 }
168
169 /**
170 * Returns the value at the back without removing it
171 * @returns The back element or undefined if deque is empty
172 */
173 backValue(): T | undefined {
174 return this.back?.value;
175 }
176
177 /**
178 * Returns the number of elements in the deque
179 * @returns Size of the deque
180 */
181 getSize(): number {
182 return this.size;
183 }
184
185 /**
186 * Checks if the deque is empty
187 * @returns True if deque has no elements, false otherwise
188 */
189 isEmpty(): boolean {
190 return this.size === 0;
191 }
192}
193
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through the array once with a for loop from index 0 to n-1. Within each iteration:
- The
popleft()
operation occurs at most once per element (each element can only be removed from the front of the deque once) - The
f[i] = nums[i] + f[q[0]]
operation takesO(1)
time - The while loop with
pop()
operations might seem like it could takeO(n)
time per iteration, but each element is added to the deque exactly once and removed at most once throughout the entire algorithm, amortizing toO(1)
per iteration - The
append(i)
operation takesO(1)
time
Since each element is processed a constant number of times across all iterations, the total time complexity is O(n)
.
Space Complexity: O(n)
The algorithm uses:
- An array
f
of sizen
to store the dynamic programming results:O(n)
space - A deque
q
that can contain at mostmin(k+1, n)
elements at any time:O(min(k+1, n))
space, which isO(n)
in the worst case whenk ≥ n-1
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle the Starting Position Correctly
Pitfall: A common mistake is not properly initializing the DP for the starting position or trying to calculate dp[0]
using the deque operations, which would fail since the deque might be empty or contain invalid indices.
Why it happens: The logic for positions 1 through n-1 involves looking back at previous positions, but position 0 has no previous positions to consider.
Solution: Ensure the deque is initialized with index 0 before the loop starts, and the DP calculation works correctly even for i=0:
# Correct initialization deque_indices = deque([0]) # When i=0, the deque contains [0], so: # dp[0] = nums[0] + dp[0] = nums[0] + 0 = nums[0] # This correctly sets the starting position's score
2. Incorrect Window Size Check
Pitfall: Using the wrong condition to check if an index is outside the valid jump range:
# Wrong: doesn't account for the actual distance
if deque_indices[0] < i - k: # This is off by one!
deque_indices.popleft()
# Wrong: using length of deque instead of index difference
if len(deque_indices) > k:
deque_indices.popleft()
Why it happens: Confusion about whether to use <
or <=
, or misunderstanding that we need to check the distance between indices, not the deque size.
Solution: Use the correct condition that checks if the distance exceeds k:
# Correct: remove indices that are more than k steps away if i - deque_indices[0] > k: deque_indices.popleft()
3. Maintaining Monotonic Property Incorrectly
Pitfall: Using strict inequality when maintaining the monotonic deque:
# Wrong: using strict inequality while deque_indices and dp[deque_indices[-1]] < dp[i]: # Should be <= deque_indices.pop()
Why it happens: Not realizing that keeping equal values would be redundant since the newer index with the same value is always preferable (it stays valid longer).
Solution: Use <=
to remove indices with equal or smaller values:
# Correct: remove indices with smaller OR EQUAL dp values while deque_indices and dp[deque_indices[-1]] <= dp[i]: deque_indices.pop()
4. Accessing Empty Deque
Pitfall: The deque might become empty after the window size check, leading to an IndexError when accessing deque_indices[0]
:
# Dangerous if deque becomes empty dp[i] = nums[i] + dp[deque_indices[0]] # IndexError if deque is empty
Why it happens: Over-aggressive removal of elements or incorrect logic that empties the deque entirely.
Solution: The deque should never be empty in a correct implementation because:
- We always add the current index after processing
- We only remove indices outside the valid range
- For any position i, at least one previous position within range k should exist (except for i=0, which is handled by initialization)
However, for defensive programming:
# Defensive check (though shouldn't be necessary with correct logic) if not deque_indices: dp[i] = nums[i] # Handle edge case else: dp[i] = nums[i] + dp[deque_indices[0]]
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!