Facebook Pixel

656. Coin Path 🔒

Problem Description

You have an array coins of integers where each position represents a cost to visit that position. The array uses 1-based indexing (position 1 is the first element). Some positions may have a value of -1, which means you cannot visit them.

Starting from position 1, you need to reach position n (the last position) by making jumps. From any position i, you can jump to any position between i+1 and i+maxJump (inclusive), as long as:

  • The target position is within the array bounds
  • The target position doesn't have a value of -1

When you visit a position i, you must pay coins[i] as the cost.

Your goal is to find the path from position 1 to position n that minimizes the total cost. The output should be an array of positions (using 1-based indexing) that represents this minimum cost path.

If there are multiple paths with the same minimum cost, you need to return the lexicographically smallest path. A path is lexicographically smaller if at the first position where two paths differ, the lexicographically smaller path has a smaller position number.

For example, path [1, 2, 5] is lexicographically smaller than [1, 3, 5] because at the second position, 2 < 3.

If it's impossible to reach position n (either because position n has value -1 or there's no valid path), return an empty array.

The solution uses dynamic programming working backwards from position n. It calculates f[i] as the minimum cost to reach position n from position i. Then it reconstructs the lexicographically smallest path by greedily choosing the earliest position at each step that maintains the minimum cost.

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

Intuition

The key insight is that we need to find both the minimum cost AND the lexicographically smallest path. This dual requirement suggests we should work backwards from the destination.

Why backwards? If we work forwards, we'd need to track multiple paths with the same cost to ensure we get the lexicographically smallest one. But working backwards simplifies this - we first find the minimum cost to reach the end from each position, then reconstruct the path going forward.

Think of it like this: once we know the minimum cost from each position to the end, we can make greedy choices when building our path. Starting from position 1, at each step we pick the earliest valid next position that maintains our minimum cost path.

The dynamic programming recurrence is straightforward: f[i] = min(f[j] + coins[i]) for all reachable positions j from i. Here f[i] represents the minimum cost to go from position i to position n.

The clever part comes in path reconstruction. Since we want the lexicographically smallest path, we iterate through positions from left to right. At each step, we greedily take the first position whose minimum cost equals our remaining budget. This works because:

  1. We know the total minimum cost from f[0]
  2. As we visit each position, we subtract its cost from our budget
  3. The first position we encounter with cost equal to our budget must be part of the lexicographically smallest path

For example, if positions 2 and 3 both lead to paths with the same total cost, we'll encounter position 2 first in our left-to-right scan and include it in our answer, ensuring lexicographic minimality.

Learn more about Dynamic Programming patterns.

Solution Approach

The implementation uses dynamic programming with backward traversal followed by forward path reconstruction.

Step 1: Initial Check

if coins[-1] == -1:
    return []

First, we check if the destination position has value -1. If it does, it's impossible to reach it, so we return an empty array immediately.

Step 2: Initialize DP Array

n = len(coins)
f = [inf] * n
f[-1] = coins[-1]

We create a DP array f where f[i] represents the minimum cost to reach position n from position i. Initialize all values to infinity except the last position, which costs coins[-1] (the cost of being at the destination).

Step 3: Fill DP Table Backwards

for i in range(n - 2, -1, -1):
    if coins[i] != -1:
        for j in range(i + 1, min(n, i + maxJump + 1)):
            if f[i] > f[j] + coins[i]:
                f[i] = f[j] + coins[i]

Working backwards from position n-2 to 0:

  • Skip positions with value -1 (unreachable)
  • For each valid position i, check all positions we can jump to: [i+1, i+maxJump]
  • Update f[i] with the minimum cost: current position's cost plus the minimum cost from the next position

Step 4: Check Feasibility

if f[0] == inf:
    return []

If f[0] is still infinity, there's no valid path from position 1 to position n.

Step 5: Reconstruct Path

ans = []
s = f[0]
for i in range(n):
    if f[i] == s:
        s -= coins[i]
        ans.append(i + 1)

To build the lexicographically smallest path:

  • Start with the total minimum cost s = f[0]
  • Iterate through positions from left to right
  • When we find a position i where f[i] == s, it means this position is on our optimal path
  • Add this position to our answer (converting to 1-based indexing with i + 1)
  • Subtract the cost of this position from our remaining budget s

This greedy approach guarantees the lexicographically smallest path because we always choose the earliest position that maintains the minimum cost. The algorithm ensures that the path we construct has exactly the minimum cost we calculated, and by processing positions in order, we get the lexicographically smallest such path.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Input: coins = [1, 2, 4, -1, 2], maxJump = 2

We need to find the minimum cost path from position 1 to position 5 (using 1-based indexing).

Step 1: Initial Check

  • coins[-1] = 2 (not -1), so we can potentially reach the destination.

Step 2: Initialize DP Array

  • n = 5
  • f = [inf, inf, inf, inf, 2]
  • f[4] = coins[4] = 2 (cost to be at position 5)

Step 3: Fill DP Table Backwards

Working from position 4 (index 3) backwards:

Position 4 (index 3):

  • coins[3] = -1, so skip this position (cannot visit)

Position 3 (index 2):

  • coins[2] = 4 (valid position)
  • Can jump to positions 4 or 5 (indices 3 or 4)
  • Position 4 has coins[3] = -1, so skip
  • Position 5: f[2] = min(inf, f[4] + coins[2]) = 2 + 4 = 6
  • Update: f = [inf, inf, 6, inf, 2]

Position 2 (index 1):

  • coins[1] = 2 (valid position)
  • Can jump to positions 3 or 4 (indices 2 or 3)
  • Position 3: f[1] = min(inf, f[2] + coins[1]) = 6 + 2 = 8
  • Position 4 has coins[3] = -1, so skip
  • Update: f = [inf, 8, 6, inf, 2]

Position 1 (index 0):

  • coins[0] = 1 (valid position)
  • Can jump to positions 2 or 3 (indices 1 or 2)
  • Position 2: f[0] = min(inf, f[1] + coins[0]) = 8 + 1 = 9
  • Position 3: f[0] = min(9, f[2] + coins[0]) = min(9, 6 + 1) = 7
  • Update: f = [7, 8, 6, inf, 2]

Step 4: Check Feasibility

  • f[0] = 7 (not infinity), so a valid path exists.

Step 5: Reconstruct Path

Starting with s = f[0] = 7, iterate through positions:

Position 1 (index 0):

  • f[0] = 7, which equals s
  • Add position 1 to answer: ans = [1]
  • Update budget: s = 7 - coins[0] = 7 - 1 = 6

Position 2 (index 1):

  • f[1] = 8, which doesn't equal s = 6
  • Skip this position

Position 3 (index 2):

  • f[2] = 6, which equals s
  • Add position 3 to answer: ans = [1, 3]
  • Update budget: s = 6 - coins[2] = 6 - 4 = 2

Position 4 (index 3):

  • f[3] = inf, which doesn't equal s = 2
  • Skip this position

Position 5 (index 4):

  • f[4] = 2, which equals s
  • Add position 5 to answer: ans = [1, 3, 5]
  • Update budget: s = 2 - coins[4] = 2 - 2 = 0

Final Answer: [1, 3, 5] with total cost 7

This path visits positions 1→3→5 with costs 1+4+2=7. Note that we could also go 1→2→3→5 with costs 1+2+4+2=9, but our algorithm correctly finds the minimum cost path. The greedy reconstruction ensures we get the lexicographically smallest path among all minimum cost paths.

Solution Implementation

1class Solution:
2    def cheapestJump(self, coins: List[int], maxJump: int) -> List[int]:
3        # If the last position is unreachable (marked as -1), no valid path exists
4        if coins[-1] == -1:
5            return []
6      
7        n = len(coins)
8      
9        # dp[i] represents the minimum cost to reach the end from position i
10        dp = [float('inf')] * n
11        dp[-1] = coins[-1]  # Base case: cost at the last position is just its coin value
12      
13        # Fill the dp table from right to left (backward)
14        for i in range(n - 2, -1, -1):
15            # Skip positions marked as unreachable (-1)
16            if coins[i] != -1:
17                # Try all possible jumps from position i
18                for j in range(i + 1, min(n, i + maxJump + 1)):
19                    # Update minimum cost if we found a better path
20                    if dp[i] > dp[j] + coins[i]:
21                        dp[i] = dp[j] + coins[i]
22      
23        # If starting position has infinite cost, no valid path exists
24        if dp[0] == float('inf'):
25            return []
26      
27        # Reconstruct the path with lexicographically smallest sequence
28        path = []
29        remaining_cost = dp[0]
30      
31        # Greedily select positions that maintain the optimal cost
32        for i in range(n):
33            if dp[i] == remaining_cost:
34                remaining_cost -= coins[i]
35                path.append(i + 1)  # Convert to 1-indexed position
36      
37        return path
38
1class Solution {
2    public List<Integer> cheapestJump(int[] coins, int maxJump) {
3        int n = coins.length;
4        List<Integer> result = new ArrayList<>();
5      
6        // If the last position is blocked, no valid path exists
7        if (coins[n - 1] == -1) {
8            return result;
9        }
10      
11        // dp[i] represents the minimum cost to reach the end from position i
12        int[] dp = new int[n];
13        final int INFINITY = 1 << 30;  // Large value representing unreachable state
14        Arrays.fill(dp, INFINITY);
15      
16        // Base case: cost at the last position is just the coin value there
17        dp[n - 1] = coins[n - 1];
18      
19        // Fill dp array from right to left (backward dynamic programming)
20        for (int currentPos = n - 2; currentPos >= 0; currentPos--) {
21            // Skip blocked positions (coins[i] == -1)
22            if (coins[currentPos] != -1) {
23                // Try all possible jumps from current position
24                int maxReachablePos = Math.min(n, currentPos + maxJump + 1);
25                for (int nextPos = currentPos + 1; nextPos < maxReachablePos; nextPos++) {
26                    // Update minimum cost if we find a cheaper path
27                    if (dp[currentPos] > dp[nextPos] + coins[currentPos]) {
28                        dp[currentPos] = dp[nextPos] + coins[currentPos];
29                    }
30                }
31            }
32        }
33      
34        // If start position cannot reach the end, return empty list
35        if (dp[0] == INFINITY) {
36            return result;
37        }
38      
39        // Reconstruct the path with minimum cost and lexicographically smallest indices
40        int remainingCost = dp[0];
41        for (int position = 0; position < n; position++) {
42            // Check if current position is part of the optimal path
43            if (dp[position] == remainingCost) {
44                remainingCost -= coins[position];
45                result.add(position + 1);  // Convert to 1-indexed
46            }
47        }
48      
49        return result;
50    }
51}
52
1class Solution {
2public:
3    vector<int> cheapestJump(vector<int>& coins, int maxJump) {
4        int n = coins.size();
5        vector<int> result;
6      
7        // If the last position is unreachable (marked as -1), return empty result
8        if (coins[n - 1] == -1) {
9            return result;
10        }
11      
12        // dp[i] represents the minimum cost to reach the end from position i
13        vector<int> dp(n);
14        const int INF = 1 << 30;  // Large value representing impossible/infinite cost
15      
16        // Base case: cost at the last position is just the coin value there
17        dp[n - 1] = coins[n - 1];
18      
19        // Fill dp table from right to left (backward)
20        for (int i = n - 2; i >= 0; --i) {
21            dp[i] = INF;  // Initialize with impossible cost
22          
23            // If current position is reachable (not -1)
24            if (coins[i] != -1) {
25                // Try all possible jumps from current position
26                for (int j = i + 1; j < min(n, i + maxJump + 1); ++j) {
27                    // Update minimum cost: current coin cost + cost to reach end from j
28                    dp[i] = min(dp[i], dp[j] + coins[i]);
29                }
30            }
31        }
32      
33        // If starting position cannot reach the end, return empty result
34        if (dp[0] == INF) {
35            return result;
36        }
37      
38        // Reconstruct the path with minimum cost
39        // Start from position 0 with total cost dp[0]
40        int remainingCost = dp[0];
41        for (int i = 0; i < n; ++i) {
42            // If current position's dp value matches remaining cost,
43            // it means this position is part of the optimal path
44            if (dp[i] == remainingCost) {
45                remainingCost -= coins[i];  // Deduct current position's cost
46                result.push_back(i + 1);     // Add position to result (1-indexed)
47            }
48        }
49      
50        return result;
51    }
52};
53
1/**
2 * Finds the cheapest path from index 0 to the last index with the minimum cost.
3 * Returns the 1-indexed positions of the path.
4 * @param coins - Array where coins[i] represents the cost at position i, -1 means unreachable
5 * @param maxJump - Maximum number of positions that can be jumped forward at once
6 * @returns Array of 1-indexed positions forming the cheapest path, empty if no valid path exists
7 */
8function cheapestJump(coins: number[], maxJump: number): number[] {
9    const arrayLength: number = coins.length;
10    const resultPath: number[] = [];
11  
12    // If the last position is unreachable, return empty array
13    if (coins[arrayLength - 1] === -1) {
14        return resultPath;
15    }
16  
17    // Initialize infinity value for comparison
18    const INFINITY: number = 1 << 30;
19  
20    // Dynamic programming array to store minimum cost to reach the end from each position
21    const minCostToEnd: number[] = new Array(arrayLength).fill(INFINITY);
22  
23    // Base case: cost at the last position is just the coin value at that position
24    minCostToEnd[arrayLength - 1] = coins[arrayLength - 1];
25  
26    // Fill the DP array from right to left
27    for (let currentIndex = arrayLength - 2; currentIndex >= 0; currentIndex--) {
28        // Skip if current position is unreachable
29        if (coins[currentIndex] !== -1) {
30            // Check all possible jump destinations from current position
31            const maxReachableIndex: number = Math.min(arrayLength, currentIndex + maxJump + 1);
32            for (let jumpTarget = currentIndex + 1; jumpTarget < maxReachableIndex; jumpTarget++) {
33                // Update minimum cost for current position
34                minCostToEnd[currentIndex] = Math.min(
35                    minCostToEnd[currentIndex], 
36                    minCostToEnd[jumpTarget] + coins[currentIndex]
37                );
38            }
39        }
40    }
41  
42    // If starting position cannot reach the end, return empty array
43    if (minCostToEnd[0] === INFINITY) {
44        return resultPath;
45    }
46  
47    // Reconstruct the path by following positions that contribute to the minimum cost
48    let remainingCost: number = minCostToEnd[0];
49    for (let position = 0; position < arrayLength; position++) {
50        // If this position is part of the optimal path
51        if (minCostToEnd[position] === remainingCost) {
52            // Subtract the cost at this position
53            remainingCost -= coins[position];
54            // Add 1-indexed position to result
55            resultPath.push(position + 1);
56        }
57    }
58  
59    return resultPath;
60}
61

Time and Space Complexity

Time Complexity: O(n * maxJump)

The algorithm consists of two main parts:

  • The dynamic programming phase: We iterate through all n positions from right to left. For each position i, we check up to maxJump positions ahead (from i+1 to min(n, i + maxJump + 1)). This gives us O(n * maxJump) operations.
  • The path reconstruction phase: We iterate through the array once more with O(n) operations.

Therefore, the overall time complexity is O(n * maxJump) + O(n) = O(n * maxJump).

Space Complexity: O(n)

The algorithm uses:

  • Array f of size n to store the minimum cost from each position to the end: O(n)
  • Array ans to store the result path, which in the worst case contains all positions: O(n)
  • A few variables (s, i, j) that use constant space: O(1)

The total space complexity is O(n) + O(n) + O(1) = O(n).

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

Common Pitfalls

Pitfall 1: Incorrect Path Reconstruction Logic

The current path reconstruction logic has a critical flaw. It assumes that any position with dp[i] == remaining_cost is part of the optimal path, but this doesn't account for reachability constraints. A position might have the "correct" cost value but not be reachable from the previous position in our path due to the maxJump limitation.

Example of the bug:

  • coins = [1, 2, 4, -1, 2], maxJump = 1
  • After DP calculation: dp = [9, 8, 6, inf, 2]
  • Path reconstruction would incorrectly produce [1, 2, 3, 5]
  • But position 5 is not reachable from position 3 with maxJump = 1!

Solution: Track the previous position and ensure each next position is within valid jump range:

# Correct path reconstruction
path = [1]  # Start from position 1 (1-indexed)
current_pos = 0
remaining_cost = dp[0] - coins[0]

while current_pos < n - 1:
    # Find the lexicographically smallest valid next position
    for next_pos in range(current_pos + 1, min(n, current_pos + maxJump + 1)):
        if coins[next_pos] != -1 and dp[next_pos] == remaining_cost:
            path.append(next_pos + 1)  # Convert to 1-indexed
            remaining_cost -= coins[next_pos]
            current_pos = next_pos
            break
    else:
        # No valid next position found (shouldn't happen if dp[0] != inf)
        return []

return path

Pitfall 2: Not Handling Edge Cases Properly

The code doesn't handle certain edge cases correctly:

  1. Single element array: When n = 1, the path should be [1] if coins[0] != -1
  2. Starting position is -1: Should return empty array immediately

Solution: Add edge case checks at the beginning:

def cheapestJump(self, coins: List[int], maxJump: int) -> List[int]:
    n = len(coins)
  
    # Edge case: starting position is unreachable
    if coins[0] == -1:
        return []
  
    # Edge case: single element
    if n == 1:
        return [1] if coins[0] != -1 else []
  
    # Edge case: destination is unreachable
    if coins[-1] == -1:
        return []
  
    # Continue with main algorithm...

Pitfall 3: Confusion Between 0-indexed and 1-indexed Positions

The problem uses 1-based indexing for output, but the implementation uses 0-based indexing internally. This can lead to off-by-one errors if not handled consistently throughout the code.

Solution: Be explicit about conversions and add comments to clarify indexing:

# When adding to path, always convert: internal_index + 1
path.append(current_pos + 1)  # Convert 0-indexed to 1-indexed

# When processing jump ranges, remember maxJump is inclusive
for j in range(i + 1, min(n, i + maxJump + 1)):  # i+1 to i+maxJump inclusive
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More