LeetCode 3919 - Minimum Cost to Move Between Indices
We are given a strictly increasing array nums. Each position represents an index in the array, and we want to move between indices as cheaply as possible. For every index x, a special neighbor called closest(x) is defined.
Difficulty: 🟡 Medium
Topics: Array, Greedy, Prefix Sum
Solution
LeetCode 3919 - Minimum Cost to Move Between Indices
Problem Understanding
We are given a strictly increasing array nums. Each position represents an index in the array, and we want to move between indices as cheaply as possible.
For every index x, a special neighbor called closest(x) is defined. Since the array is strictly increasing, each index can only have up to two adjacent neighbors:
x - 1x + 1
Among those adjacent neighbors, we choose the one whose value difference with nums[x] is smallest. If both differences are equal, we choose the smaller index.
From any index x, we have two possible types of moves:
- Move directly to any index
yat cost:
abs(nums[x] - nums[y])
- Move to
closest(x)at cost:
1
For each query [li, ri], we must compute the minimum total cost required to travel from index li to index ri.
The constraints are very large:
n ≤ 100,000q ≤ 100,000
This immediately rules out running a shortest path algorithm independently for every query.
The array being strictly increasing is extremely important because it means:
abs(nums[i] - nums[j]) = nums[j] - nums[i]wheneverj > i- Adjacent differences completely determine every
closestrelationship - The graph structure becomes highly predictable
Important edge cases include:
- Moving left versus moving right.
- Boundary indices, which only have one adjacent neighbor.
- Ties when both adjacent differences are equal.
- Queries where
li == ri, whose answer must be zero. - Very large coordinate values up to
10^9, requiring 64 bit arithmetic internally.
Approaches
Brute Force
A direct interpretation is to build the graph.
Each index has:
- One special edge to
closest(i)with weight1. - Edges to every other index with weight
abs(nums[i] - nums[j]).
Then for every query we could run Dijkstra's algorithm.
This produces the correct answer because Dijkstra finds the shortest path in a weighted graph with nonnegative edge weights.
However, the graph contains:
O(n²)
edges due to the direct move option between every pair of indices.
With n = 100,000, this is completely infeasible in both memory and time.
Key Insight
The crucial observation is that the array is sorted.
Suppose we want to move from a smaller index to a larger index.
Any direct jump from i to j costs:
nums[j] - nums[i]
because the array is increasing.
Consider moving only through adjacent indices.
For every adjacent pair (i, i+1):
- If
closest(i) = i+1, then moving right across that edge costs1. - Otherwise the cheapest way to cross is the direct difference:
nums[i+1] - nums[i]
The optimal path from left to right never needs to overshoot and come back. The minimum cost becomes the sum of independent costs of crossing each adjacent boundary.
Similarly, for moving from right to left:
- If
closest(i) = i-1, crossing costs1. - Otherwise crossing costs
nums[i] - nums[i-1].
Therefore every query reduces to a range sum problem.
We precompute:
- Forward crossing costs.
- Backward crossing costs.
- Prefix sums of both arrays.
Then every query is answered in O(1) time.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(q · n² log n) | O(n²) | Build full graph and run shortest path |
| Optimal | O(n + q) | O(n) | Precompute crossing costs and prefix sums |
Algorithm Walkthrough
Step 1: Compute the closest index for every position
For every index:
- Index
0must choose1. - Index
n-1must choosen-2. - Interior indices compare:
nums[i] - nums[i-1]
nums[i+1] - nums[i]
The smaller difference determines the closest neighbor.
If the differences are equal, choose the smaller index, namely i-1.
Step 2: Build forward crossing costs
For every adjacent boundary between i and i+1:
If:
closest(i) = i+1
then moving across this boundary costs 1.
Otherwise the cheapest crossing cost is:
nums[i+1] - nums[i]
Store this value in:
forward[i]
Step 3: Build backward crossing costs
For every adjacent boundary between i-1 and i:
If:
closest(i) = i-1
then moving left costs 1.
Otherwise it costs:
nums[i] - nums[i-1]
Store this value in:
backward[i]
Step 4: Build prefix sums
Construct:
prefForward
prefBackward
where:
prefForward[k]
contains the total forward crossing cost before position k.
Likewise for prefBackward.
These prefix sums allow constant time range queries.
Step 5: Answer queries
For a query (l, r):
If:
l < r
the answer is:
prefForward[r] - prefForward[l]
because we cross boundaries:
l, l+1, ..., r-1
If:
l > r
the answer is:
prefBackward[l] - prefBackward[r]
because we cross boundaries:
r+1, r+2, ..., l
If:
l == r
the answer is zero.
Why it works
For any movement direction, every path from one side of an adjacent boundary to the other must pay at least the minimum crossing cost of that boundary. Because the array is strictly increasing, crossing different boundaries becomes independent, and the optimal route is obtained by summing the minimum crossing cost of each boundary exactly once. The forward and backward arrays store these optimal boundary crossing costs, so their prefix sums produce the minimum total cost for every query.
Python Solution
class Solution:
def minCost(self, nums: list[int], queries: list[list[int]]) -> list[int]:
n = len(nums)
closest = [0] * n
closest[0] = 1
closest[n - 1] = n - 2
for i in range(1, n - 1):
left_diff = nums[i] - nums[i - 1]
right_diff = nums[i + 1] - nums[i]
if left_diff <= right_diff:
closest[i] = i - 1
else:
closest[i] = i + 1
forward = [0] * (n - 1)
for i in range(n - 1):
if closest[i] == i + 1:
forward[i] = 1
else:
forward[i] = nums[i + 1] - nums[i]
backward = [0] * n
for i in range(1, n):
if closest[i] == i - 1:
backward[i] = 1
else:
backward[i] = nums[i] - nums[i - 1]
pref_forward = [0] * (n + 1)
for i in range(n - 1):
pref_forward[i + 1] = pref_forward[i] + forward[i]
pref_backward = [0] * (n + 1)
for i in range(1, n):
pref_backward[i + 1] = pref_backward[i] + backward[i]
answer = []
for left, right in queries:
if left < right:
answer.append(
pref_forward[right] - pref_forward[left]
)
elif left > right:
answer.append(
pref_backward[left + 1] - pref_backward[right + 1]
)
else:
answer.append(0)
return answer
The implementation follows the algorithm directly.
The first section computes the closest array using only adjacent differences. Since the array is strictly increasing, the difference to the left neighbor and the difference to the right neighbor are easy to compute.
The next two sections construct the forward and backward crossing cost arrays. These arrays encode the cheapest cost needed to cross each adjacent boundary in a specific direction.
After that, prefix sums are built. Once prefix sums exist, every query becomes a simple subtraction of two prefix values.
Finally, each query is processed independently in constant time.
Go Solution
func minCost(nums []int, queries [][]int) []int {
n := len(nums)
closest := make([]int, n)
closest[0] = 1
closest[n-1] = n - 2
for i := 1; i < n-1; i++ {
leftDiff := nums[i] - nums[i-1]
rightDiff := nums[i+1] - nums[i]
if leftDiff <= rightDiff {
closest[i] = i - 1
} else {
closest[i] = i + 1
}
}
forward := make([]int64, n-1)
for i := 0; i < n-1; i++ {
if closest[i] == i+1 {
forward[i] = 1
} else {
forward[i] = int64(nums[i+1] - nums[i])
}
}
backward := make([]int64, n)
for i := 1; i < n; i++ {
if closest[i] == i-1 {
backward[i] = 1
} else {
backward[i] = int64(nums[i] - nums[i-1])
}
}
prefForward := make([]int64, n+1)
for i := 0; i < n-1; i++ {
prefForward[i+1] = prefForward[i] + forward[i]
}
prefBackward := make([]int64, n+1)
for i := 1; i < n; i++ {
prefBackward[i+1] = prefBackward[i] + backward[i]
}
ans := make([]int, len(queries))
for idx, q := range queries {
l, r := q[0], q[1]
var cost int64
if l < r {
cost = prefForward[r] - prefForward[l]
} else if l > r {
cost = prefBackward[l+1] - prefBackward[r+1]
} else {
cost = 0
}
ans[idx] = int(cost)
}
return ans
}
The Go version mirrors the Python implementation closely.
The primary difference is that prefix sums and intermediate costs are stored using int64. Although the final answer fits within the problem constraints, using 64 bit arithmetic prevents overflow when summing many large differences.
Slices are used for all arrays, and no special handling for nil slices is required because all arrays are allocated with known sizes.
Worked Examples
Example 1
nums = [-5, -2, 3]
Differences:
| Index | Left Diff | Right Diff | Closest |
|---|---|---|---|
| 0 | - | 3 | 1 |
| 1 | 3 | 5 | 0 |
| 2 | 5 | - | 1 |
So:
closest = [1, 0, 1]
Forward costs:
| Boundary | Cost |
|---|---|
| 0→1 | 1 |
| 1→2 | 5 |
forward = [1, 5]
prefForward = [0, 1, 6, 6]
Backward costs:
| Boundary | Cost |
|---|---|
| 1→0 | 1 |
| 2→1 | 1 |
backward = [0, 1, 1]
prefBackward = [0, 0, 1, 2]
Query [0,2]:
prefForward[2] - prefForward[0]
= 6 - 0
= 6
Query [2,0]:
prefBackward[3] - prefBackward[1]
= 2 - 0
= 2
Query [1,2]:
prefForward[2] - prefForward[1]
= 6 - 1
= 5
Result:
[6, 2, 5]
Example 2
nums = [0,2,3,9]
Closest indices:
closest = [1,2,1,2]
Forward costs:
[1,1,6]
Prefix:
[0,1,2,8,8]
Backward costs:
[0,2,1,1]
Prefix:
[0,0,2,3,4]
Query [3,0]:
4 - 0 = 4
Query [1,2]:
2 - 1 = 1
Query [2,0]:
3 - 0 = 3
Result:
[4,1,3]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + q) | One preprocessing pass and O(1) per query |
| Space | O(n) | Closest arrays, cost arrays, and prefix sums |
The preprocessing stage scans the array a constant number of times, which requires linear time. Each query is answered using a constant number of prefix sum lookups and subtractions. Therefore the total running time is O(n + q). The auxiliary arrays used during preprocessing require linear space.
Test Cases
sol = Solution()
# Example 1
assert sol.minCost(
[-5, -2, 3],
[[0, 2], [2, 0], [1, 2]]
) == [6, 2, 5]
# Example 2
assert sol.minCost(
[0, 2, 3, 9],
[[3, 0], [1, 2], [2, 0]]
) == [4, 1, 3]
# Smallest valid array
assert sol.minCost(
[1, 10],
[[0, 1], [1, 0]]
) == [1, 1]
# Same start and end
assert sol.minCost(
[1, 5, 10],
[[0, 0], [1, 1], [2, 2]]
) == [0, 0, 0]
# Equal adjacent differences, tie goes left
assert sol.minCost(
[0, 5, 10],
[[2, 0]]
) == [2]
# Large gaps
assert sol.minCost(
[0, 1, 1000, 2000],
[[0, 3]]
) == [1001]
# Forward travel across many boundaries
assert sol.minCost(
[0, 2, 3, 4, 100],
[[0, 4]]
) == [98]
# Backward travel across many boundaries
assert sol.minCost(
[0, 2, 3, 4, 100],
[[4, 0]]
) == [4]
# Mixed queries
assert sol.minCost(
[0, 3, 7, 20],
[[0, 3], [3, 0], [1, 2]]
) == [17, 4, 4]
Test Summary
| Test | Why |
|---|---|
| Example 1 | Verifies sample behavior |
| Example 2 | Verifies sample behavior in both directions |
| Two elements | Smallest valid input size |
| Same index queries | Checks zero cost answers |
| Equal adjacent differences | Validates tie-breaking rule |
| Large gaps | Ensures difference costs are handled correctly |
| Long forward path | Tests multiple forward boundary crossings |
| Long backward path | Tests multiple backward boundary crossings |
| Mixed queries | Exercises both directions simultaneously |
Edge Cases
Query Starts and Ends at the Same Index
When li == ri, no movement is necessary. A common bug is accidentally performing a prefix subtraction that crosses an invalid range. The implementation explicitly checks for this condition and returns zero immediately.
Equal Adjacent Differences
Suppose:
nums = [0, 5, 10]
For index 1, both neighboring differences equal 5. The problem specifies that ties must choose the smaller index. The implementation uses:
if left_diff <= right_diff
which correctly selects the left neighbor during ties.
Boundary Indices
The first and last indices only have one adjacent neighbor. Attempting to apply the normal interior logic would cause out of bounds access. The implementation handles index 0 and index n-1 separately before processing interior indices.
Very Large Values
Array values may be as large as 10^9 in magnitude. Summing many differences across a path can exceed 32 bit integer limits. The Go implementation stores accumulated costs in int64, ensuring safe arithmetic during prefix sum computation.