2297. Jump Game VIII đź”’
Problem Description
You have an array nums
of integers and start at index 0. You want to reach the last index (n-1) by jumping between positions, but there are specific rules about which jumps are allowed.
From any index i
, you can jump to a later index j
(where i < j
) only if one of these conditions is met:
-
Ascending jump:
nums[i] <= nums[j]
AND all elements between them are strictly less thannums[i]
. In other words,nums[i]
must be a local maximum compared to all elements betweeni
andj
. -
Descending jump:
nums[i] > nums[j]
AND all elements between them are greater than or equal tonums[i]
. In other words,nums[i]
must be a local minimum compared to all elements betweeni
andj
.
Each index has an associated cost given in the costs
array. When you jump to index j
, you must pay costs[j]
.
Your goal is to find the minimum total cost to reach the last index (n-1) starting from index 0.
For example, if you have nums = [3, 2, 4, 2, 1]
:
- From index 0 (value 3), you could jump to index 2 (value 4) because 3 ≤ 4 and the value between them (2) is less than 3.
- From index 0 (value 3), you could also jump to index 4 (value 1) because 3 > 1 and all values between them (2, 4, 2) are ≥ 3... wait, 2 < 3, so this jump would NOT be valid.
The algorithm uses monotonic stacks to efficiently find valid jump destinations for each position, then uses dynamic programming to compute the minimum cost path from index 0 to index n-1.
Intuition
The key insight is recognizing that valid jumps follow a specific pattern: we can only jump to positions that are either the next "peak" or the next "valley" relative to our current position.
Think about what makes a jump valid:
- For an ascending jump (
nums[i] <= nums[j]
), all intermediate values must be smaller thannums[i]
. This meansj
must be the first position afteri
where the value rises back up to or abovenums[i]
. - For a descending jump (
nums[i] > nums[j]
), all intermediate values must be at leastnums[i]
. This meansj
must be the first position afteri
where the value drops belownums[i]
.
This observation leads us to realize that from any position i
, we can only jump to very specific positions - not just any arbitrary later index. We need to find:
- The next position where the value is greater than or equal to
nums[i]
(for ascending jumps) - The next position where the value is less than
nums[i]
(for descending jumps)
Finding these "next greater/smaller element" positions is a classic problem that can be solved efficiently using monotonic stacks. A monotonic stack maintains elements in either increasing or decreasing order, allowing us to find the next element that breaks the pattern in O(n)
time.
Once we know all valid jumps from each position, the problem becomes a shortest path problem in a directed graph where:
- Nodes are array indices
- Edges represent valid jumps
- Edge weights are the costs of jumping to the destination index
Since we're looking for minimum cost and can only move forward (from smaller to larger indices), we can use dynamic programming. We track f[i]
as the minimum cost to reach index i
, starting with f[0] = 0
. For each position we can reach, we update its minimum cost based on all positions that can jump to it.
The combination of monotonic stacks (to build the graph) and dynamic programming (to find the minimum cost path) gives us an efficient O(n)
solution.
Learn more about Stack, Graph, Dynamic Programming, Shortest Path and Monotonic Stack patterns.
Solution Approach
The solution consists of two main phases: building the jump graph using monotonic stacks, and finding the minimum cost path using dynamic programming.
Phase 1: Building the Jump Graph with Monotonic Stacks
We use two monotonic stacks to find valid jump destinations from each position:
-
Finding next greater or equal elements (for ascending jumps):
- Traverse the array from right to left
- Maintain a stack where elements are in decreasing order from bottom to top
- For each position
i
, pop elements from the stack whilenums[stk[-1]] < nums[i]
- After popping, if the stack is not empty,
stk[-1]
is the nearest position to the right wherenums[j] >= nums[i]
- Add this position to the adjacency list
g[i]
- Push current index
i
onto the stack
-
Finding next smaller elements (for descending jumps):
- Again traverse from right to left with a fresh stack
- Maintain a stack where elements are in increasing order from bottom to top
- For each position
i
, pop elements whilenums[stk[-1]] >= nums[i]
- After popping, if the stack is not empty,
stk[-1]
is the nearest position to the right wherenums[j] < nums[i]
- Add this position to
g[i]
The monotonic stack approach ensures that for each position i
, we find the first valid jump destination to the right that satisfies our conditions. The time complexity for building the graph is O(n)
since each element is pushed and popped at most once.
Phase 2: Dynamic Programming for Minimum Cost
With the adjacency list g
constructed, we solve for the minimum cost:
- Initialize
f[i] = inf
for all positions exceptf[0] = 0
(starting position has no cost) - Iterate through positions from
0
ton-1
- For each position
i
, check all positionsj
ing[i]
(positions we can jump to fromi
) - Update the minimum cost:
f[j] = min(f[j], f[i] + costs[j])
- The answer is
f[n-1]
, the minimum cost to reach the last index
The DP transition works because:
- We process positions in order, ensuring that when we reach position
i
, we already know the minimum cost to get there - For each reachable position
j
fromi
, we update its minimum cost by considering the path throughi
- Since we can only jump forward, this guarantees we find the optimal solution
The overall time complexity is O(n)
for building the graph plus O(n + edges)
for the DP, where the number of edges is at most 2n
(each position can have at most 2 outgoing edges). Thus, the total time complexity is O(n)
.
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 trace through a concrete example with nums = [3, 2, 4, 2, 1]
and costs = [0, 10, 15, 20, 25]
.
Phase 1: Building the Jump Graph
Finding next greater or equal elements (ascending jumps):
- Start from right to left with an empty stack
- i=4 (value 1): Stack is empty, no valid jump. Push 4. Stack: [4]
- i=3 (value 2): nums[4]=1 < 2, pop 4. Stack empty, no valid jump. Push 3. Stack: [3]
- i=2 (value 4): nums[3]=2 < 4, pop 3. Stack empty, no valid jump. Push 2. Stack: [2]
- i=1 (value 2): nums[2]=4 ≥ 2, so we can jump from 1→2. Push 1. Stack: [2, 1]
- i=0 (value 3): nums[1]=2 < 3, pop 1. nums[2]=4 ≥ 3, so we can jump from 0→2. Push 0. Stack: [2, 0]
Finding next smaller elements (descending jumps):
- Start from right to left with a fresh empty stack
- i=4 (value 1): Stack empty, no valid jump. Push 4. Stack: [4]
- i=3 (value 2): nums[4]=1 < 2, so we can jump from 3→4. Push 3. Stack: [4, 3]
- i=2 (value 4): nums[3]=2 < 4, so we can jump from 2→3. Push 2. Stack: [4, 3, 2]
- i=1 (value 2): nums[2]=4 ≥ 2, pop 2. nums[3]=2 ≥ 2, pop 3. nums[4]=1 < 2, so we can jump from 1→4. Push 1. Stack: [4, 1]
- i=0 (value 3): nums[1]=2 < 3, so we can jump from 0→1. Push 0. Stack: [4, 1, 0]
Adjacency list g
:
- g[0] = [2, 1] (can jump to indices 2 or 1)
- g[1] = [2, 4] (can jump to indices 2 or 4)
- g[2] = [3] (can jump to index 3)
- g[3] = [4] (can jump to index 4)
- g[4] = [] (last index, no jumps)
Phase 2: Dynamic Programming
Initialize f = [0, inf, inf, inf, inf]
-
i=0: f[0]=0
- Jump to j=2: f[2] = min(inf, 0 + 15) = 15
- Jump to j=1: f[1] = min(inf, 0 + 10) = 10
- Now f = [0, 10, 15, inf, inf]
-
i=1: f[1]=10
- Jump to j=2: f[2] = min(15, 10 + 15) = 15
- Jump to j=4: f[4] = min(inf, 10 + 25) = 35
- Now f = [0, 10, 15, inf, 35]
-
i=2: f[2]=15
- Jump to j=3: f[3] = min(inf, 15 + 20) = 35
- Now f = [0, 10, 15, 35, 35]
-
i=3: f[3]=35
- Jump to j=4: f[4] = min(35, 35 + 25) = 35
- Now f = [0, 10, 15, 35, 35]
-
i=4: No outgoing edges
The minimum cost to reach index 4 is f[4] = 35, achieved by the path 0→1→4 with total cost 0 + 10 + 25 = 35.
Solution Implementation
1from typing import List
2from collections import defaultdict
3from math import inf
4
5
6class Solution:
7 def minCost(self, nums: List[int], costs: List[int]) -> int:
8 n = len(nums)
9 # Graph to store valid jumps from each index
10 graph = defaultdict(list)
11
12 # Build edges for jumps to the next greater or equal element
13 stack = []
14 for i in range(n - 1, -1, -1):
15 # Pop elements smaller than current element
16 while stack and nums[stack[-1]] < nums[i]:
17 stack.pop()
18 # If stack has elements, top element is the next greater or equal
19 if stack:
20 graph[i].append(stack[-1])
21 stack.append(i)
22
23 # Build edges for jumps to the next smaller element
24 stack = []
25 for i in range(n - 1, -1, -1):
26 # Pop elements greater than or equal to current element
27 while stack and nums[stack[-1]] >= nums[i]:
28 stack.pop()
29 # If stack has elements, top element is the next smaller
30 if stack:
31 graph[i].append(stack[-1])
32 stack.append(i)
33
34 # Dynamic programming array to store minimum cost to reach each index
35 dp = [inf] * n
36 dp[0] = 0 # Starting position has zero cost
37
38 # Calculate minimum cost for each position
39 for i in range(n):
40 # Update cost for all reachable positions from current index
41 for j in graph[i]:
42 dp[j] = min(dp[j], dp[i] + costs[j])
43
44 return dp[n - 1]
45
1class Solution {
2 public long minCost(int[] nums, int[] costs) {
3 int n = nums.length;
4
5 // Create adjacency list to store reachable indices from each position
6 List<Integer>[] reachableIndices = new List[n];
7 Arrays.setAll(reachableIndices, index -> new ArrayList<>());
8
9 // First pass: Find the next greater element for each position
10 // Using a monotonic decreasing stack (stores indices)
11 Deque<Integer> monotonicStack = new ArrayDeque<>();
12 for (int currentIndex = n - 1; currentIndex >= 0; currentIndex--) {
13 // Pop elements from stack that are smaller than current element
14 while (!monotonicStack.isEmpty() && nums[monotonicStack.peek()] < nums[currentIndex]) {
15 monotonicStack.pop();
16 }
17 // The remaining top element (if exists) is the next greater element
18 if (!monotonicStack.isEmpty()) {
19 reachableIndices[currentIndex].add(monotonicStack.peek());
20 }
21 monotonicStack.push(currentIndex);
22 }
23
24 // Clear stack for reuse
25 monotonicStack.clear();
26
27 // Second pass: Find the next smaller or equal element for each position
28 // Using a monotonic strictly increasing stack (stores indices)
29 for (int currentIndex = n - 1; currentIndex >= 0; currentIndex--) {
30 // Pop elements from stack that are greater than or equal to current element
31 while (!monotonicStack.isEmpty() && nums[monotonicStack.peek()] >= nums[currentIndex]) {
32 monotonicStack.pop();
33 }
34 // The remaining top element (if exists) is the next smaller element
35 if (!monotonicStack.isEmpty()) {
36 reachableIndices[currentIndex].add(monotonicStack.peek());
37 }
38 monotonicStack.push(currentIndex);
39 }
40
41 // Dynamic programming array to store minimum cost to reach each index
42 long[] minCostToReach = new long[n];
43 // Initialize with a large value (effectively infinity)
44 Arrays.fill(minCostToReach, 1L << 60);
45 // Starting position has zero cost
46 minCostToReach[0] = 0;
47
48 // Process each position and update costs for reachable positions
49 for (int fromIndex = 0; fromIndex < n; fromIndex++) {
50 // For each position reachable from current index
51 for (int toIndex : reachableIndices[fromIndex]) {
52 // Update minimum cost to reach the destination index
53 minCostToReach[toIndex] = Math.min(
54 minCostToReach[toIndex],
55 minCostToReach[fromIndex] + costs[toIndex]
56 );
57 }
58 }
59
60 // Return the minimum cost to reach the last position
61 return minCostToReach[n - 1];
62 }
63}
64
1class Solution {
2public:
3 long long minCost(vector<int>& nums, vector<int>& costs) {
4 int n = nums.size();
5
6 // adjacency list to store valid jumps from each index
7 vector<vector<int>> adjacencyList(n);
8
9 // find the next greater element to the right for each position
10 stack<int> monotonicStack;
11 for (int i = n - 1; i >= 0; --i) {
12 // pop elements from stack that are smaller than current element
13 while (!monotonicStack.empty() && nums[monotonicStack.top()] < nums[i]) {
14 monotonicStack.pop();
15 }
16 // if stack has elements, top element is the next greater or equal element
17 if (!monotonicStack.empty()) {
18 adjacencyList[i].push_back(monotonicStack.top());
19 }
20 monotonicStack.push(i);
21 }
22
23 // find the next smaller or equal element to the right for each position
24 monotonicStack = stack<int>(); // reset the stack
25 for (int i = n - 1; i >= 0; --i) {
26 // pop elements from stack that are greater than or equal to current element
27 while (!monotonicStack.empty() && nums[monotonicStack.top()] >= nums[i]) {
28 monotonicStack.pop();
29 }
30 // if stack has elements, top element is the next smaller element
31 if (!monotonicStack.empty()) {
32 adjacencyList[i].push_back(monotonicStack.top());
33 }
34 monotonicStack.push(i);
35 }
36
37 // dp array to store minimum cost to reach each index
38 const long long INF = 1e18;
39 vector<long long> minCostToReach(n, INF);
40 minCostToReach[0] = 0; // starting position has zero cost
41
42 // iterate through each position and update costs for reachable positions
43 for (int currentIndex = 0; currentIndex < n; ++currentIndex) {
44 for (int nextIndex : adjacencyList[currentIndex]) {
45 // update minimum cost to reach nextIndex
46 minCostToReach[nextIndex] = min(minCostToReach[nextIndex],
47 minCostToReach[currentIndex] + costs[nextIndex]);
48 }
49 }
50
51 // return the minimum cost to reach the last index
52 return minCostToReach[n - 1];
53 }
54};
55
1/**
2 * Finds the minimum cost to reach the last element by jumping through the array
3 * @param nums - Array of numbers representing values at each position
4 * @param costs - Array of costs for reaching each position
5 * @returns The minimum cost to reach the last element
6 */
7function minCost(nums: number[], costs: number[]): number {
8 const arrayLength: number = nums.length;
9
10 // Graph adjacency list to store valid jump destinations from each index
11 const jumpGraph: number[][] = Array.from({ length: arrayLength }, () => []);
12
13 // Stack for monotonic stack operations
14 const monotonicStack: number[] = [];
15
16 // First pass: Find next greater element for each position (right to left)
17 // This identifies positions we can jump to where nums[j] > nums[i]
18 for (let currentIndex = arrayLength - 1; currentIndex >= 0; --currentIndex) {
19 // Pop elements from stack that are smaller than current element
20 while (monotonicStack.length && nums[monotonicStack[monotonicStack.length - 1]] < nums[currentIndex]) {
21 monotonicStack.pop();
22 }
23
24 // If stack has elements, top element is the next greater element
25 if (monotonicStack.length) {
26 jumpGraph[currentIndex].push(monotonicStack[monotonicStack.length - 1]);
27 }
28
29 // Add current index to stack
30 monotonicStack.push(currentIndex);
31 }
32
33 // Clear the stack for second pass
34 monotonicStack.length = 0;
35
36 // Second pass: Find next smaller or equal element for each position (right to left)
37 // This identifies positions we can jump to where nums[j] <= nums[i]
38 for (let currentIndex = arrayLength - 1; currentIndex >= 0; --currentIndex) {
39 // Pop elements from stack that are greater than or equal to current element
40 while (monotonicStack.length && nums[monotonicStack[monotonicStack.length - 1]] >= nums[currentIndex]) {
41 monotonicStack.pop();
42 }
43
44 // If stack has elements, top element is the next smaller element
45 if (monotonicStack.length) {
46 jumpGraph[currentIndex].push(monotonicStack[monotonicStack.length - 1]);
47 }
48
49 // Add current index to stack
50 monotonicStack.push(currentIndex);
51 }
52
53 // Dynamic programming array to store minimum cost to reach each position
54 const minCostToReach: number[] = Array.from({ length: arrayLength }, () => Infinity);
55
56 // Starting position has zero cost
57 minCostToReach[0] = 0;
58
59 // Calculate minimum cost for each position using dynamic programming
60 for (let fromIndex = 0; fromIndex < arrayLength; ++fromIndex) {
61 // For each valid jump destination from current position
62 for (const toIndex of jumpGraph[fromIndex]) {
63 // Update minimum cost if jumping from fromIndex to toIndex is cheaper
64 minCostToReach[toIndex] = Math.min(
65 minCostToReach[toIndex],
66 minCostToReach[fromIndex] + costs[toIndex]
67 );
68 }
69 }
70
71 // Return the minimum cost to reach the last element
72 return minCostToReach[arrayLength - 1];
73}
74
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of three main parts:
-
First monotonic stack traversal (lines 5-11): Each element is pushed and popped from the stack at most once, resulting in
O(n)
operations. -
Second monotonic stack traversal (lines 13-19): Similarly, each element is pushed and popped at most once, giving
O(n)
operations. -
Dynamic programming update (lines 21-24): The outer loop runs
n
times. For the inner loop, we need to count the total number of edges in graphg
. Each indexi
can have at most 2 neighbors added (one from each stack traversal). Therefore, the total number of edges across all vertices isO(n)
, making the total work in this sectionO(n)
.
Overall time complexity: O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
The space usage includes:
-
Graph
g
: A defaultdict storing adjacency lists. In the worst case, it storesO(n)
edges total, requiringO(n)
space. -
Stack
stk
: At any point, the stack can contain at mostn
elements, usingO(n)
space. -
DP array
f
: An array of sizen
, requiringO(n)
space.
Overall space complexity: O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Jump Conditions
A critical pitfall is misinterpreting what constitutes a valid jump. The monotonic stack approach finds the next greater/equal or smaller element, but this doesn't guarantee all intermediate elements satisfy the jump conditions.
The Problem:
- For an ascending jump from
i
toj
, ALL elements between them must be strictly less thannums[i]
- For a descending jump from
i
toj
, ALL elements between them must be greater than or equal tonums[i]
- The current solution only finds the next element that satisfies the inequality but doesn't verify intermediate elements
Example where this fails:
nums = [3, 1, 4, 2] # From index 0 (value 3), the next greater/equal is index 2 (value 4) # But this is NOT a valid ascending jump because we need ALL elements # between them (just 1 in this case) to be < 3, which is satisfied. # However, consider: nums = [3, 4, 1, 5] # From index 0 (value 3), next greater/equal is index 1 (value 4) # This IS a valid jump (no elements between them) # But from index 0, we cannot jump to index 3 (value 5) even though 3 ≤ 5 # because element at index 1 (value 4) is NOT less than 3
Solution:
def minCost(self, nums: List[int], costs: List[int]) -> int:
n = len(nums)
graph = defaultdict(list)
# For each position, find ALL valid jump destinations
for i in range(n - 1):
# Check ascending jumps
for j in range(i + 1, n):
if nums[i] <= nums[j]:
# Check if all elements between i and j are < nums[i]
valid = True
for k in range(i + 1, j):
if nums[k] >= nums[i]:
valid = False
break
if valid:
graph[i].append(j)
break # Only need the first valid ascending jump
# Check descending jumps
for j in range(i + 1, n):
if nums[i] > nums[j]:
# Check if all elements between i and j are >= nums[i]
valid = True
for k in range(i + 1, j):
if nums[k] < nums[i]:
valid = False
break
if valid:
graph[i].append(j)
break # Only need the first valid descending jump
# DP remains the same
dp = [inf] * n
dp[0] = 0
for i in range(n):
for j in graph[i]:
dp[j] = min(dp[j], dp[i] + costs[j])
return dp[n - 1]
2. Incorrect Handling of Unreachable Positions
The Problem: If the last index cannot be reached from index 0, the current solution returns inf
, which might not be the expected behavior.
Solution: Add validation to check if the destination is reachable:
result = dp[n - 1] if result == inf: return -1 # or raise an exception depending on requirements return result
3. Stack Order Confusion
The Problem: The monotonic stack maintains a specific order (decreasing or increasing), and it's easy to confuse which comparison to use when popping elements.
Key Points to Remember:
- For finding next greater/equal: Pop while
nums[stack[-1]] < nums[i]
(maintain decreasing stack) - For finding next smaller: Pop while
nums[stack[-1]] >= nums[i]
(maintain increasing stack)
Getting these conditions reversed will result in finding the wrong jump destinations and incorrect results.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
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
Want a Structured Path to Master System Design Too? Don’t Miss This!