3221. Maximum Array Hopping Score II 🔒
Problem Description
You are given an array nums
and need to find the maximum score you can achieve by jumping through the array.
Starting from index 0, you can make "hops" to reach the last element of the array. In each hop, you can jump from your current index i
to any index j
where j > i
. When you make such a jump, you earn a score of (j - i) * nums[j]
.
Your goal is to find the maximum total score you can accumulate by making a sequence of jumps from index 0 to the last index of the array.
For example, if you're at index i
and jump to index j
, you gain (j - i) * nums[j]
points. The distance you jump (j - i)
is multiplied by the value at your destination nums[j]
. You continue making jumps until you reach the final index, and the sum of all scores from your jumps is your total score.
The challenge is to determine which sequence of jumps will give you the maximum possible total score.
Intuition
When we're at any position i
, we need to decide where to jump next. The score for jumping to position j
is (j - i) * nums[j]
. This means we get more points by either jumping further (larger j - i
) or jumping to a position with a higher value (larger nums[j]
).
Let's think about when we would want to jump to a particular position. If we're at position i
and considering positions ahead, we should ask: "Is there a better position to jump to later?"
Consider two positions j1
and j2
where i < j1 < j2
. If nums[j1] <= nums[j2]
, would we ever want to jump to j1
? The answer is no! Here's why:
- If we jump directly to
j2
, we get score(j2 - i) * nums[j2]
- If we jump to
j1
first thenj2
, we get(j1 - i) * nums[j1] + (j2 - j1) * nums[j2]
The direct jump gives us (j2 - i) * nums[j2]
while the two-hop path gives us (j1 - i) * nums[j1] + (j2 - j1) * nums[j2]
. Since nums[j1] <= nums[j2]
, we can see that replacing nums[j1]
with nums[j2]
in the first term would only increase our score. This means the direct jump is always better or equal.
This insight tells us that we should only consider jumping to positions that have no greater or equal value ahead of them. In other words, we only jump to positions that form a decreasing sequence when viewed from right to left.
This naturally leads to using a monotonic decreasing stack. As we traverse the array, we maintain positions whose values are strictly decreasing. When we encounter a new position with a value greater than or equal to the stack top, we remove the stack top because it's no longer a candidate for jumping to - we'd rather jump to this new, better position instead.
The positions that remain in our stack at the end represent the optimal jumping points that will maximize our score.
Learn more about Stack, Greedy and Monotonic Stack patterns.
Solution Approach
We implement the solution using a monotonic stack to identify the optimal jumping positions.
Building the Monotonic Stack:
We traverse the array nums
from left to right, maintaining a stack stk
that stores indices of elements in a monotonically decreasing order (based on their values).
For each position i
with value nums[i]
:
- While the stack is not empty and
nums[stk[-1]] <= nums[i]
, we pop the top element from the stack - After popping all smaller or equal elements, we push index
i
onto the stack
This process ensures that the stack contains indices of elements that form a strictly decreasing sequence. These are the positions we should jump to for maximum score.
Calculating the Maximum Score:
After building the stack, we have identified all optimal jumping positions. Now we calculate the total score:
- Initialize
ans = 0
(to store the total score) andi = 0
(current position) - For each index
j
in the stack (from bottom to top):- Add the score for jumping from position
i
to positionj
:ans += nums[j] * (j - i)
- Update current position:
i = j
- Add the score for jumping from position
The algorithm works because:
- The stack contains positions in increasing order of indices (since we pushed them left to right)
- Each position in the stack represents an optimal jump destination
- We jump to these positions sequentially, accumulating the maximum possible score
Time Complexity: O(n)
where n
is the length of the array, as each element is pushed and popped from the stack at most once.
Space Complexity: O(n)
for storing the stack in the worst case when the array is strictly decreasing.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the array nums = [1, 3, 1, 5]
.
Step 1: Build the Monotonic Decreasing Stack
We traverse the array from left to right, maintaining indices whose values form a strictly decreasing sequence:
- i = 0,
nums[0] = 1
: Stack is empty, push 0. Stack:[0]
- i = 1,
nums[1] = 3
: Compare with stack top:nums[0] = 1 <= 3
, so pop 0. Stack becomes empty, then push 1. Stack:[1]
- i = 2,
nums[2] = 1
: Compare with stack top:nums[1] = 3 > 1
, so just push 2. Stack:[1, 2]
- i = 3,
nums[3] = 5
: Compare with stack top:nums[2] = 1 <= 5
, so pop 2. Stack:[1]
. Compare again:nums[1] = 3 <= 5
, so pop 1. Stack becomes empty, then push 3. Stack:[3]
Final stack: [3]
Step 2: Calculate Maximum Score
Now we jump through the positions in our stack:
- Start at position
i = 0
- Jump to position
j = 3
(the only element in our stack) - Score for this jump:
(3 - 0) * nums[3] = 3 * 5 = 15
Total maximum score: 15
Why is this optimal?
Let's verify by checking other possible paths:
- Path 1:
0 → 3
gives score3 * 5 = 15
✓ (our solution) - Path 2:
0 → 1 → 3
gives score1 * 3 + 2 * 5 = 3 + 10 = 13
- Path 3:
0 → 2 → 3
gives score2 * 1 + 1 * 5 = 2 + 5 = 7
- Path 4:
0 → 1 → 2 → 3
gives score1 * 3 + 1 * 1 + 1 * 5 = 3 + 1 + 5 = 9
Our algorithm correctly identified that jumping directly to position 3 yields the maximum score of 15. The key insight is that since nums[3] = 5
is the largest value and appears at the end, we want to maximize the distance multiplier by jumping to it directly from the start.
Solution Implementation
1class Solution:
2 def maxScore(self, nums: List[int]) -> int:
3 # Stack to maintain indices of elements in decreasing order
4 # This helps us find the "dominant" elements that contribute to the score
5 monotonic_stack = []
6
7 # Build a decreasing monotonic stack
8 # Each element in the stack will be greater than all elements after it
9 for current_index, current_value in enumerate(nums):
10 # Remove indices from stack if their values are less than or equal to current
11 # This ensures stack maintains strictly decreasing values
12 while monotonic_stack and nums[monotonic_stack[-1]] <= current_value:
13 monotonic_stack.pop()
14
15 # Add current index to the stack
16 monotonic_stack.append(current_index)
17
18 # Calculate the total score
19 total_score = 0
20 previous_index = 0
21
22 # Process each index in the monotonic stack
23 # These represent the "dominant" elements that contribute to the score
24 for current_stack_index in monotonic_stack:
25 # Add contribution of current element
26 # Multiply element value by the range it dominates
27 contribution = nums[current_stack_index] * (current_stack_index - previous_index)
28 total_score += contribution
29
30 # Update previous index for next iteration
31 previous_index = current_stack_index
32
33 return total_score
34
1class Solution {
2 public long maxScore(int[] nums) {
3 // Stack to maintain indices of elements in decreasing order
4 Deque<Integer> stack = new ArrayDeque<>();
5
6 // Build a monotonic decreasing stack
7 // Keep only indices where nums[index] forms a decreasing sequence
8 for (int i = 0; i < nums.length; i++) {
9 // Remove indices from stack if their values are less than or equal to current element
10 while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
11 stack.pop();
12 }
13 // Add current index to stack
14 stack.push(i);
15 }
16
17 // Calculate the maximum score
18 long maxScore = 0;
19 long previousIndex = 0;
20
21 // Process the stack from bottom to top (largest to smallest indices)
22 while (!stack.isEmpty()) {
23 // Get the index from the bottom of the stack
24 int currentIndex = stack.pollLast();
25
26 // Add to score: (range length) * (value at current index)
27 // The range is from previousIndex to currentIndex (exclusive)
28 maxScore += (currentIndex - previousIndex) * nums[currentIndex];
29
30 // Update previous index for next iteration
31 previousIndex = currentIndex;
32 }
33
34 return maxScore;
35 }
36}
37
1class Solution {
2public:
3 long long maxScore(vector<int>& nums) {
4 // Monotonic decreasing stack to store indices of selected elements
5 // Each element in the stack will be greater than all elements after it
6 vector<int> selectedIndices;
7
8 // Build the monotonic decreasing stack
9 for (int currentIndex = 0; currentIndex < nums.size(); ++currentIndex) {
10 // Remove elements from stack that are less than or equal to current element
11 // This maintains the strictly decreasing property
12 while (!selectedIndices.empty() &&
13 nums[selectedIndices.back()] <= nums[currentIndex]) {
14 selectedIndices.pop_back();
15 }
16 // Add current index to the stack
17 selectedIndices.push_back(currentIndex);
18 }
19
20 // Calculate the total score based on selected indices
21 long long totalScore = 0;
22 int previousIndex = 0;
23
24 // For each selected index, calculate contribution to score
25 // Score contribution = (index difference) * value at index
26 for (int currentSelectedIndex : selectedIndices) {
27 totalScore += static_cast<long long>(currentSelectedIndex - previousIndex) *
28 nums[currentSelectedIndex];
29 previousIndex = currentSelectedIndex;
30 }
31
32 return totalScore;
33 }
34};
35
1/**
2 * Calculates the maximum score by finding increasing elements and computing weighted sums.
3 * The score is calculated by multiplying each selected element by its distance from the previous selected element.
4 *
5 * @param nums - Array of numbers to process
6 * @returns The maximum score
7 */
8function maxScore(nums: number[]): number {
9 // Stack to maintain indices of elements in increasing order
10 const indicesStack: number[] = [];
11
12 // Build a monotonic increasing stack of indices
13 for (let currentIndex = 0; currentIndex < nums.length; currentIndex++) {
14 // Remove indices from stack while their values are less than or equal to current value
15 // This ensures we only keep indices of strictly increasing elements
16 while (indicesStack.length > 0 &&
17 nums[indicesStack[indicesStack.length - 1]] <= nums[currentIndex]) {
18 indicesStack.pop();
19 }
20 // Add current index to the stack
21 indicesStack.push(currentIndex);
22 }
23
24 // Calculate the maximum score using the selected indices
25 let maxScoreResult = 0;
26 let previousIndex = 0;
27
28 // For each selected index, add its contribution to the total score
29 // Contribution = (distance from previous index) * value at current index
30 for (const currentSelectedIndex of indicesStack) {
31 maxScoreResult += (currentSelectedIndex - previousIndex) * nums[currentSelectedIndex];
32 previousIndex = currentSelectedIndex;
33 }
34
35 return maxScoreResult;
36}
37
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input array nums
.
- The first loop iterates through each element in
nums
exactly once with indexi
from0
ton-1
. - Inside this loop, the while loop might pop elements from the stack. However, each element can be pushed onto the stack at most once and popped at most once throughout the entire execution. Therefore, the total number of push and pop operations across all iterations is at most
2n
. - The second loop iterates through the elements in
stk
, which contains at mostn
elements (in the worst case when the array is strictly increasing). - Overall, the total operations are bounded by
O(n) + O(n) = O(n)
.
Space Complexity: O(n)
where n
is the length of the input array nums
.
- The stack
stk
stores indices of elements from the input array. - In the worst case (when the input array is strictly increasing), all
n
indices will be stored in the stack. - Additional variables
ans
,i
, andj
use constant spaceO(1)
. - Therefore, the total space complexity is
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Jump Mechanics
The Problem: Many developers initially interpret this as finding the maximum value we can achieve by jumping to the last index in one sequence. They might think we need to reach the last index and stop there, similar to classic jump game problems.
Why It Happens: The problem statement mentions "reaching the last element" which can be misleading. In reality, we're accumulating scores from all jumps, and the last jump destination doesn't have to be the final array element.
The Fix: Recognize that the monotonic stack identifies ALL positions that contribute to the maximum score. The algorithm naturally handles the entire array range, and the last element in the stack effectively represents the "dominant" element for the remaining portion of the array.
Pitfall 2: Using Non-Strict Inequality in Stack Comparison
The Problem: Using nums[monotonic_stack[-1]] < current_value
instead of nums[monotonic_stack[-1]] <= current_value
when popping from the stack.
# Incorrect - keeps equal values in stack while monotonic_stack and nums[monotonic_stack[-1]] < current_value: monotonic_stack.pop() # Correct - removes equal values too while monotonic_stack and nums[monotonic_stack[-1]] <= current_value: monotonic_stack.pop()
Why It Matters: If we keep equal values in the stack, we might jump to positions with the same value unnecessarily. Since jumping further with the same value gives a better score (j-i) * nums[j]
, we should always prefer the rightmost occurrence of any value.
Example: For array [1, 3, 3, 5]
, keeping both index 1 and 2 (both have value 3) would be suboptimal. We should only keep index 2.
Pitfall 3: Incorrect Score Calculation Range
The Problem: Forgetting to update the previous_index
or incorrectly calculating the range that each element "dominates."
# Incorrect - using wrong range calculation for current_stack_index in monotonic_stack: contribution = nums[current_stack_index] * current_stack_index # Wrong! total_score += contribution # Correct - each element dominates from previous position to its position for current_stack_index in monotonic_stack: contribution = nums[current_stack_index] * (current_stack_index - previous_index) total_score += contribution previous_index = current_stack_index
Why It Matters: Each element in the monotonic stack represents the best jump destination for a range of starting positions. The score contribution should be based on how many positions can optimally jump to this element, not just its absolute index.
Pitfall 4: Attempting Dynamic Programming First
The Problem: Trying to solve with DP by defining dp[i]
as the maximum score to reach index i
.
# Inefficient O(n²) approach
dp = [0] * len(nums)
for j in range(1, len(nums)):
for i in range(j):
dp[j] = max(dp[j], dp[i] + (j - i) * nums[j])
Why It's Suboptimal: While this works, it has O(n²) time complexity. The monotonic stack solution elegantly reduces this to O(n) by recognizing that we only need to consider "dominant" elements that form a decreasing sequence.
The Insight: The key observation is that if nums[j] >= nums[k]
for j < k
, we would never want to jump to position j
instead of position k
from any position i < j
, because (k-i) * nums[k] >= (j-i) * nums[j]
when nums[k] >= nums[j]
.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!