Facebook Pixel

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:

  1. Ascending jump: nums[i] <= nums[j] AND all elements between them are strictly less than nums[i]. In other words, nums[i] must be a local maximum compared to all elements between i and j.

  2. Descending jump: nums[i] > nums[j] AND all elements between them are greater than or equal to nums[i]. In other words, nums[i] must be a local minimum compared to all elements between i and j.

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.

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

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 than nums[i]. This means j must be the first position after i where the value rises back up to or above nums[i].
  • For a descending jump (nums[i] > nums[j]), all intermediate values must be at least nums[i]. This means j must be the first position after i where the value drops below nums[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:

  1. The next position where the value is greater than or equal to nums[i] (for ascending jumps)
  2. 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:

  1. 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 while nums[stk[-1]] < nums[i]
    • After popping, if the stack is not empty, stk[-1] is the nearest position to the right where nums[j] >= nums[i]
    • Add this position to the adjacency list g[i]
    • Push current index i onto the stack
  2. 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 while nums[stk[-1]] >= nums[i]
    • After popping, if the stack is not empty, stk[-1] is the nearest position to the right where nums[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 except f[0] = 0 (starting position has no cost)
  • Iterate through positions from 0 to n-1
  • For each position i, check all positions j in g[i] (positions we can jump to from i)
  • 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 from i, we update its minimum cost by considering the path through i
  • 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 Evaluator

Example 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:

  1. First monotonic stack traversal (lines 5-11): Each element is pushed and popped from the stack at most once, resulting in O(n) operations.

  2. Second monotonic stack traversal (lines 13-19): Similarly, each element is pushed and popped at most once, giving O(n) operations.

  3. 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 graph g. Each index i can have at most 2 neighbors added (one from each stack traversal). Therefore, the total number of edges across all vertices is O(n), making the total work in this section O(n).

Overall time complexity: O(n) + O(n) + O(n) = O(n)

Space Complexity: O(n)

The space usage includes:

  1. Graph g: A defaultdict storing adjacency lists. In the worst case, it stores O(n) edges total, requiring O(n) space.

  2. Stack stk: At any point, the stack can contain at most n elements, using O(n) space.

  3. DP array f: An array of size n, requiring O(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 to j, ALL elements between them must be strictly less than nums[i]
  • For a descending jump from i to j, ALL elements between them must be greater than or equal to nums[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.

Discover Your Strengths and Weaknesses: Take Our 5-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