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.
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:
- We know the total minimum cost from
f[0]
- As we visit each position, we subtract its cost from our budget
- 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
wheref[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 EvaluatorExample 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 equalss
- 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 equals = 6
- Skip this position
Position 3 (index 2):
f[2] = 6
, which equalss
- 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 equals = 2
- Skip this position
Position 5 (index 4):
f[4] = 2
, which equalss
- 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 positioni
, we check up tomaxJump
positions ahead (fromi+1
tomin(n, i + maxJump + 1)
). This gives usO(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 sizen
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:
- Single element array: When
n = 1
, the path should be[1]
ifcoins[0] != -1
- 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
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!