1793. Maximum Score of a Good Subarray


Problem Description

Given an array nums of integers and an integer k, we need to find the maximum score of a "good" subarray. A subarray is defined by its start and end indices (i, j), and its score is calculated as the minimum value in the subarray multiplied by the subarray's length, which is (j - i + 1). A "good" subarray is one where the index k is included in the subarray; therefore, i <= k <= j.

The problem asks for the maximum score that can be achieved by any "good" subarray. To solve this problem, we must efficiently find subarrays that include k and also have high scores, which are a product of its minimum element and its length.

Intuition

To find the maximum possible score of a good subarray, we can use a two-step approach:

  1. First, we need to precompute for each index in the array, the nearest indices on the left and on the right that have a value lesser than the value at the current index. To perform this, we use two passes with the help of a stack.

    In the first pass, from left to right, we use the stack to track indices with increasing values. For each index i, if there's a lesser value than nums[i], we pop from the stack until we find the correct position where the value at nums[i] should be. The element that would be at the top of the stack after popping would be the nearest smaller element on the left. We store this index in an array left[].

    Similarly, we perform another pass, this time from right to left to fill out the right[] array for the nearest smaller element on the right.

  2. With left[] and right[] arrays at hand, we can iterate over all indices of the array again, and for each index, we check if the good subarray criteria are met i.e., left[i] < k < right[i]. If the criteria are met for an index i, we calculate the score for the subarray using nums[i] as the minimum value and (right[i] - left[i] - 1) as the length of the subarray, and we keep track of the maximum score found so far.

This method works because we are iterating through all possible subarrays that include the index k while efficiently computing their minimums using the preprocessed left and right arrays, avoiding redundant calculations.

Learn more about Stack, Two Pointers, Binary Search and Monotonic Stack patterns.

Solution Approach

The solution uses a monotonic stack and two auxiliary arrays (left and right) to find the maximum score of a good subarray. A monotonic stack is a stack where the elements are arranged according to a clear hierarchical order, either strictly increasing or decreasing. This characteristic of the monotonic stack is a key component of the solution and here is a detailed walk-through:

  1. Auxiliary Arrays (left and right):

    • Create two arrays left and right of the same length as the input array nums, initialized to -1 and n respectively, where n is the length of nums. These arrays will store for each element in nums the index of the nearest smaller element on the left and on the right, respectively.
  2. Finding Nearest Smaller Elements on the Left:

    • Initialize an empty list stk to be used as a stack.
    • Iterate over each index i (from 0 to n-1) of nums:
      • While the stack is not empty and the top element of the stack (nums[stk[-1]]) is greater than or equal to the current element (nums[i]), pop elements from the stack since they are not the nearest smaller elements for the current index i.
      • If the stack is not empty after the popping process, the top element of the stack (stk[-1]) is the nearest smaller element on the left of the current element. We record this index in left[i].
      • Push the current index i onto the stack.
  3. Finding Nearest Smaller Elements on the Right:

    • Clear the stack stk for reuse in this step.
    • Iterate over each index i starting from the last index (n-1) to the first index (0):
      • While the stack is not empty and the top element of the stack (nums[stk[-1]]) is greater than the current element (nums[i]), pop elements from the stack.
      • If the stack is not empty after popping, the top element of the stack (stk[-1]) is the nearest smaller element on the right of the current element. We record this index in right[i].
      • Push the current index i onto the stack.
  4. Calculating the Maximum Score:

    • Initialize ans to 0 which will store the result.
    • Iterate over each index i of nums:
      • Check if i can be a starting index for a good subarray by ensuring the conditions left[i] < k < right[i] are satisfied.
      • If i can be a starting index for a good subarray, calculate the score as the product of nums[i] (minimum value for this subarray) and the length of the subarray (right[i] - left[i] - 1).
      • Update ans if the calculated score is greater than the current value of ans.
  5. Return the Maximum Score:

    • After iterating over all indices, ans holds the maximum score for a good subarray, and we return it as the result.

The algorithms and data structures used in this solution—namely, the stack for finding the nearest smaller elements and the efficient iteration over the indices—ensure that every good subarray is considered, and the maximum score is calculated without redundant computations, leading to an optimal solution.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's illustrate the solution approach using a small example. Suppose our input array nums is [3, 1, 5, 2, 4], and k is 3 (which means nums[k] is 2).

  1. Auxiliary Arrays (left and right):

    • Initialize left to [-1, -1, -1, -1, -1]
    • Initialize right to [5, 5, 5, 5, 5]
    • Length of nums is 5.
  2. Finding Nearest Smaller Elements on the Left:

    • stk is [].
    • We iterate over the indices and build left:
      • i = 0: Stack is empty, push 0.
      • i = 1: nums[1] is less than nums[0], pop 0 from stack, push 1.
      • i = 2: nums[2] is greater than nums[1], push 2.
      • i = 3: nums[3] is less than nums[2], pop 2, nums[3] is less than nums[1], pop 1, stk is now empty, push 3.
      • i = 4: nums[4] is greater than nums[3], push 4.
    • Now, left is [-1, -1, 1, -1, 3].
  3. Finding Nearest Smaller Elements on the Right:

    • stk is cleared and is [] again.
    • We iterate in reverse and build right:
      • i = 4: Stack is empty, push 4.
      • i = 3: nums[3] is less than nums[4], push 3.
      • i = 2: nums[2] is greater than nums[3], pop 3, stack is empty, push 2.
      • i = 1: nums[1] is less than nums[2], push 1.
      • i = 0: nums[0] is greater than nums[1], pop 1, now stk is empty, push 0.
    • Now, right is [1, 2, 5, 5, 5].
  4. Calculating the Maximum Score:

    • Initialize ans to 0.
    • Iterate over each index i of nums:
      • For i = 0: left[0] < k < right[0] doesn't hold, so skip.
      • For i = 1: left[1] < k < right[1] holds (-1 < 3 < 2), score is nums[1] * (right[1] - left[1] - 1) which is 1 * (2 - (-1) - 1) equal to 1.
      • For i = 2: left[2] < k < right[2] holds (1 < 3 < 5), score is nums[2] * (right[2] - left[2] - 1) which is 5 * (5 - 1 - 1) equal to 15. Update ans = 15.
      • For i = 3: left[3] < k < right[3] holds (-1 < 3 < 5), score is nums[3] * (right[3] - left[3] - 1) which is 2 * (5 - (-1) - 1) equal to 8. ans remains 15.
      • For i = 4: left[4] < k < right[4] doesn't hold, so skip.
  5. Return the Maximum Score:

    • After iterating over all indices, we found the maximum score to be 15, so we return 15.

Using this approach, even with a small example, we're able to find the maximum score of a "good" subarray efficiently.

Solution Implementation

1from typing import List
2
3class Solution:
4    def maximumScore(self, nums: List[int], k: int) -> int:
5        # Initialize the number of elements in the list.
6        num_elements = len(nums)
7
8        # Initialize lists to keep track of the nearest smaller number to the left and right.
9        nearest_smaller_left = [-1] * num_elements
10        nearest_smaller_right = [num_elements] * num_elements
11      
12        # Use a stack to find the nearest smaller to the left for each element.
13        stack = []
14        for index, value in enumerate(nums):
15            # Pop elements from the stack until you find a smaller element.
16            while stack and nums[stack[-1]] >= value:
17                stack.pop()
18            # If the stack is not empty, the top element is the nearest smaller to the left.
19            if stack:
20                nearest_smaller_left[index] = stack[-1]
21            # Push the current index onto the stack.
22            stack.append(index)
23      
24        # Clear the stack to reuse it for finding the nearest smaller to the right.
25        stack.clear()
26      
27        # Iterate in reverse to find the nearest smaller to the right for each element.
28        for index in range(num_elements - 1, -1, -1):
29            value = nums[index]
30            # Pop elements from the stack until you find a smaller element.
31            while stack and nums[stack[-1]] > value:
32                stack.pop()
33            # If the stack is not empty, the top element is the nearest smaller to the right.
34            if stack:
35                nearest_smaller_right[index] = stack[-1]
36            # Push the current index onto the stack.
37            stack.append(index)
38      
39        # Initial maximum score to 0.
40        max_score = 0
41      
42        # Check each element in the list if they can form valid windows with the given constraints.
43        for index, value in enumerate(nums):
44            # Ensure the current window contains the index k.
45            if nearest_smaller_left[index] + 1 <= k <= nearest_smaller_right[index] - 1:
46                # Calculate the score for the current window and update the maximum score.
47                max_score = max(max_score, value * (nearest_smaller_right[index] - nearest_smaller_left[index] - 1))
48      
49        # Return the maximum score found.
50        return max_score
51
1class Solution {
2    public int maximumScore(int[] nums, int k) {
3        int length = nums.length;
4      
5        // Arrays to store the index of the previous smaller element
6        int[] left = new int[length];
7      
8        // Arrays to store the index of the next smaller element
9        int[] right = new int[length];
10      
11        // Initialize all the values to the bounds
12        Arrays.fill(left, -1);
13        Arrays.fill(right, length);
14      
15        // Stack to help find the previous smaller element
16        Deque<Integer> stack = new ArrayDeque<>();
17      
18        // Find the previous smaller elements for all positions
19        for (int i = 0; i < length; ++i) {
20            while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
21                stack.pop();
22            }
23            if (!stack.isEmpty()) {
24                left[i] = stack.peek();
25            }
26            stack.push(i);
27        }
28      
29        // Clear the stack to reuse it for the next smaller elements
30        stack.clear();
31      
32        // Find the next smaller elements for all positions
33        for (int i = length - 1; i >= 0; --i) {
34            while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
35                stack.pop();
36            }
37            if (!stack.isEmpty()) {
38                right[i] = stack.peek();
39            }
40            stack.push(i);
41        }
42      
43        // Initially set the maximum score as the smallest possible value
44        int maxScore = 0;
45      
46        // Calculate the maximum score achievable
47        for (int i = 0; i < length; ++i) {
48            // Check if the current position can contribute to the score
49            if (left[i] + 1 <= k && k <= right[i] - 1) {
50                // Update the maximum score using the width times the minimum height
51                // in the range (between the next and previous smaller elements)
52                maxScore = Math.max(maxScore, nums[i] * (right[i] - left[i] - 1));
53            }
54        }
55      
56        return maxScore;
57    }
58}
59
1#include <vector>
2#include <stack>
3#include <algorithm> // For std::max
4
5class Solution {
6public:
7    int maximumScore(vector<int>& nums, int k) {
8        // Determine the size of the input vector.
9        int n = nums.size();
10
11        // Initialize left and right boundary vectors.
12        vector<int> leftBoundaries(n, -1);
13        vector<int> rightBoundaries(n, n);
14
15        // Use a stack to find the left boundaries for each element.
16        stack<int> stk;
17        for (int i = 0; i < n; ++i) {
18            while (!stk.empty() && nums[stk.top()] >= nums[i]) {
19                stk.pop();
20            }
21            if (!stk.empty()) {
22                leftBoundaries[i] = stk.top();
23            }
24            stk.push(i);
25        }
26
27        // Clear the stack for the next use.
28        stk = stack<int>();
29
30        // Use a stack to find the right boundaries for each element.
31        for (int i = n - 1; i >= 0; --i) {
32            while (!stk.empty() && nums[stk.top()] > nums[i]) {
33                stk.pop();
34            }
35            if (!stk.empty()) {
36                rightBoundaries[i] = stk.top();
37            }
38            stk.push(i);
39        }
40
41        // Initialize the answer.
42        int maxScore = 0;
43
44        // Iterate over the elements to calculate the maximum score.
45        for (int i = 0; i < n; ++i) {
46            // Check if the current element can form a valid rectangle with the index k.
47            if (leftBoundaries[i] + 1 <= k && k <= rightBoundaries[i] - 1) {
48                // Update maxScore with the larger value between the current maxScore and the
49                // area of the new valid rectangle.
50                maxScore = std::max(maxScore, nums[i] * (rightBoundaries[i] - leftBoundaries[i] - 1));
51            }
52        }
53
54        // Return the maximum score.
55        return maxScore;
56    }
57};
58
1function maximumScore(nums: number[], k: number): number {
2    // Determine the size of the input array.
3    const n: number = nums.length;
4
5    // Initialize left and right boundary arrays.
6    const leftBoundaries: number[] = new Array(n).fill(-1);
7    const rightBoundaries: number[] = new Array(n).fill(n);
8
9    // Use a stack to find the left boundaries for each element.
10    const leftStack: number[] = [];
11    for (let i = 0; i < n; ++i) {
12        while (leftStack.length !== 0 && nums[leftStack[leftStack.length - 1]] >= nums[i]) {
13            leftStack.pop();
14        }
15        if (leftStack.length !== 0) {
16            leftBoundaries[i] = leftStack[leftStack.length - 1];
17        }
18        leftStack.push(i);
19    }
20
21    // Use a stack to find the right boundaries for each element.
22    const rightStack: number[] = [];
23    for (let i = n - 1; i >= 0; --i) {
24        while (rightStack.length !== 0 && nums[rightStack[rightStack.length - 1]] > nums[i]) {
25            rightStack.pop();
26        }
27        if (rightStack.length !== 0) {
28            rightBoundaries[i] = rightStack[rightStack.length - 1];
29        }
30        rightStack.push(i);
31    }
32
33    // Initialize the answer.
34    let maxScore: number = 0;
35
36    // Iterate over the elements to calculate the maximum score.
37    for (let i = 0; i < n; ++i) {
38        // Check if the current element can form a valid rectangle with the index k.
39        if (leftBoundaries[i] + 1 <= k && k <= rightBoundaries[i] - 1) {
40            // Update maxScore with the larger value between the current maxScore and the
41            // area of the new valid rectangle.
42            maxScore = Math.max(maxScore, nums[i] * (rightBoundaries[i] - leftBoundaries[i] - 1));
43        }
44    }
45
46    // Return the maximum score.
47    return maxScore;
48}
49

Time and Space Complexity

The given Python code finds the maximum score of a subarray within the given list nums where the score is defined as the minimum integer in the subarray times the length of the subarray. The range of indices for the subarray should include the index k. The code uses a stack to find the next smaller elements for each element in the list to determine the possible subarray ranges where the element is the smallest within those ranges.

Time Complexity:

The time complexity of the code is O(n).

Here's the breakdown:

  • The first loop, where left boundaries for each element are calculated, iterates over the n elements. For each element, it performs a constant number of actions – checking and popping from the stack, and occasionally appending to the stack. While the popping might seem like it could lead to more than O(n) time, each element is added and removed from the stack at most once, which gives us O(n) for this loop.
  • The second loop is similar to the first but in reverse to find right boundaries. Its complexity is also O(n) for the same reasons.
  • The final loop again iterates over n elements and performs O(1) work for each by just checking indices and calculating the area if the conditions are met.

Summing these complexities gives us O(n) + O(n) + O(n) = O(n).

Space Complexity:

The space complexity of the code is O(n).

Here's the rationale:

  • Two additional lists are created, left and right, each of size n, giving us 2n space usage.
  • There is also a single stack used twice. However, the stack size will at most grow to n in the worst case (though not at the same time for both uses).
  • Other than these, only a constant amount of additional space is used for variables such as i, v, and ans.

As such, the dominant term is O(n), making the overall space complexity O(n) as well.

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


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which technique can we use to find the middle of a linked list?


Recommended Readings

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


Load More