LeetCode 3205 - Maximum Array Hopping Score I
The problem asks us to calculate the maximum score achievable by hopping through an array from the first element to the last. You start at index 0, and at each step, you can jump to any subsequent index j i. When you jump, you accumulate a score of (j - i) nums[j].
Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Stack, Greedy, Monotonic Stack
Solution
Problem Understanding
The problem asks us to calculate the maximum score achievable by hopping through an array from the first element to the last. You start at index 0, and at each step, you can jump to any subsequent index j > i. When you jump, you accumulate a score of (j - i) * nums[j]. The goal is to select a sequence of jumps that maximizes the total score.
The input is an array of integers nums with length between 2 and 1000, and each element is between 1 and 100,000. The constraints indicate that an O(n^2) approach could be feasible for n ≈ 1000, but an O(n log n) or O(n) approach is preferable.
Important edge cases include arrays with all elements equal, strictly increasing or decreasing elements, and arrays of minimum length (2). The problem guarantees at least two elements, so we do not need to handle empty arrays.
Approaches
Brute Force
The naive approach is to explore every possible sequence of jumps. For each index i, try jumping to every j > i, recursively calculate the maximum score from j to the end, and add (j - i) * nums[j]. This method is correct because it considers every possible sequence of jumps, but it is extremely inefficient. For an array of size n, the number of possible jump sequences is exponential (O(2^n)), making this infeasible for n up to 1000.
Optimal Approach
The key observation is that the score from jumping to index j depends only on the previous maximum score and the formula (j - i) * nums[j]. We can formulate a dynamic programming solution where dp[j] stores the maximum score to reach index j. Instead of computing dp[j] by iterating over all previous i, we notice a monotonic property: if nums[i] is large, it may dominate smaller values of nums[i] even for smaller i. Using a monotonic stack or priority queue, we can efficiently determine the best previous index i for each j.
Specifically, we can maintain a stack of candidate indices where nums[i] - i or a derived linear function is monotonic. This reduces the inner loop computation from O(n) to amortized O(1) for each index, yielding an overall O(n) solution.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n) | O(n) | Recursively try every jump sequence |
| Optimal | O(n) | O(n) | Use dynamic programming with a monotonic stack to efficiently find best previous index |
Algorithm Walkthrough
- Initialize an array
dpof sizen, wheredp[i]stores the maximum score to reach indexi. Setdp[0] = 0because we start at the first element. - Create a monotonic stack to store candidate indices that could maximize future jumps.
- Iterate through the array from index
1ton - 1. - For each index
j, remove indices from the stack that will never produce a higher score than the current candidate. This is done by comparing(dp[i] - i * nums[i])values. - Compute
dp[j]as the maximum possible score from the top of the stack:dp[j] = (j - i) * nums[j] + dp[i]whereiis the top index of the stack. - Maintain the monotonic property of the stack by removing any indices that are now worse than
j. - After processing all indices, return
dp[n - 1].
Why it works: The algorithm maintains an invariant that the stack contains only indices that could potentially maximize the score for future jumps. By keeping only relevant indices and removing dominated ones, we ensure the maximum score is always computed efficiently.
Python Solution
from typing import List
class Solution:
def maxScore(self, nums: List[int]) -> int:
n = len(nums)
dp = [0] * n
stack = [0] # store candidate indices
for j in range(1, n):
# Compute dp[j] using the top of the stack
while len(stack) >= 2:
i1, i2 = stack[-2], stack[-1]
score1 = dp[i1] + (j - i1) * nums[j]
score2 = dp[i2] + (j - i2) * nums[j]
if score2 >= score1:
stack.pop(-2) # remove the worse candidate
else:
break
dp[j] = dp[stack[-1]] + (j - stack[-1]) * nums[j]
# Maintain monotonicity for future jumps
while stack and (dp[j] - j * nums[j]) >= (dp[stack[-1]] - stack[-1] * nums[stack[-1]]):
stack.pop()
stack.append(j)
return dp[-1]
Explanation: We initialize dp and a stack of candidate indices. For each j, we compute the maximum score achievable using the best index on the stack. We maintain the stack by removing dominated indices to ensure future indices are efficiently computed.
Go Solution
func maxScore(nums []int) int {
n := len(nums)
dp := make([]int, n)
stack := []int{0}
for j := 1; j < n; j++ {
for len(stack) >= 2 {
i1, i2 := stack[len(stack)-2], stack[len(stack)-1]
score1 := dp[i1] + (j - i1) * nums[j]
score2 := dp[i2] + (j - i2) * nums[j]
if score2 >= score1 {
stack = append(stack[:len(stack)-2], stack[len(stack)-1])
} else {
break
}
}
dp[j] = dp[stack[len(stack)-1]] + (j - stack[len(stack)-1]) * nums[j]
for len(stack) > 0 && (dp[j]-j*nums[j]) >= (dp[stack[len(stack)-1]]-stack[len(stack)-1]*nums[stack[len(stack)-1]]) {
stack = stack[:len(stack)-1]
}
stack = append(stack, j)
}
return dp[n-1]
}
Go Notes: The Go implementation mirrors Python closely. We use slices for the stack and carefully handle slice manipulation. Integer arithmetic is safe due to problem constraints.
Worked Examples
Example 1: nums = [1,5,8]
| j | stack | dp[j] | Calculation |
|---|---|---|---|
| 1 | [0] | 5 | 0 + (1-0)*5 |
| 2 | [1] | 16 | 0 + (2-0)*8 |
Result: 16
Example 2: nums = [4,5,2,8,9,1,3]
| j | stack | dp[j] | Calculation |
|---|---|---|---|
| 1 | [0,1] | 5 | 0 + (1-0)*5 |
| 2 | [0,1,2] | 9 | 1 + (2-1)*2 |
| 3 | [3] | 29 | 5 + (3-1)*8 |
| 4 | [4] | 41 | 29 + (4-3)*9 |
| 5 | [4,5] | 42 | 41 + (5-4)*1 |
| 6 | [6] | 42 | 41 + (6-4)*3 |
Result: 42
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each index is pushed/popped from stack at most once |
| Space | O(n) | dp array and stack of indices |
The monotonic stack ensures that we do not iterate over all previous indices for every j, reducing naive O(n^2) complexity to O(n).
Test Cases
# Provided examples
assert Solution().maxScore([1,5,8]) == 16 # example 1
assert Solution().maxScore([4,5,2,8,9,1,3]) == 42 # example 2
# Edge cases
assert Solution().maxScore([1,1]) == 1 # minimum length
assert Solution().maxScore([100000,100000]) == 100000 # max values
assert Solution().maxScore([1,2,3,4,5]) == 16 # strictly increasing
assert Solution().maxScore([5,4,3,2,1]) == 9 # strictly decreasing
assert Solution().maxScore([1,3,1,3,1]) == 12 # alternating highs and lows
| Test | Why |
|---|---|
| [1,5,8] | Basic small array example |
| [4,5,2,8,9,1,3] | Complex sequence with skips |
| [1,1] | Minimum length |