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:
-
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 thannums[i]
, we pop from the stack until we find the correct position where the value atnums[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 arrayleft[]
.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. -
With
left[]
andright[]
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 indexi
, we calculate the score for the subarray usingnums[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:
-
Auxiliary Arrays (left and right):
- Create two arrays
left
andright
of the same length as the input arraynums
, initialized to-1
andn
respectively, wheren
is the length ofnums
. These arrays will store for each element innums
the index of the nearest smaller element on the left and on the right, respectively.
- Create two arrays
-
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 ton-1
) ofnums
:- 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 indexi
. - 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 inleft[i]
. - Push the current index
i
onto the stack.
- While the stack is not empty and the top element of the stack (
- Initialize an empty list
-
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 inright[i]
. - Push the current index
i
onto the stack.
- While the stack is not empty and the top element of the stack (
- Clear the stack
-
Calculating the Maximum Score:
- Initialize
ans
to0
which will store the result. - Iterate over each index
i
ofnums
:- Check if
i
can be a starting index for a good subarray by ensuring the conditionsleft[i] < k < right[i]
are satisfied. - If
i
can be a starting index for a good subarray, calculate the score as the product ofnums[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 ofans
.
- Check if
- Initialize
-
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.
- After iterating over all indices,
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 EvaluatorExample 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
).
-
Auxiliary Arrays (left and right):
- Initialize
left
to[-1, -1, -1, -1, -1]
- Initialize
right
to[5, 5, 5, 5, 5]
- Length of
nums
is5
.
- Initialize
-
Finding Nearest Smaller Elements on the Left:
stk
is[]
.- We iterate over the indices and build
left
:i = 0
: Stack is empty, push0
.i = 1
:nums[1]
is less thannums[0]
, pop0
from stack, push1
.i = 2
:nums[2]
is greater thannums[1]
, push2
.i = 3
:nums[3]
is less thannums[2]
, pop2
,nums[3]
is less thannums[1]
, pop1
,stk
is now empty, push3
.i = 4
:nums[4]
is greater thannums[3]
, push4
.
- Now,
left
is[-1, -1, 1, -1, 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, push4
.i = 3
:nums[3]
is less thannums[4]
, push3
.i = 2
:nums[2]
is greater thannums[3]
, pop3
, stack is empty, push2
.i = 1
:nums[1]
is less thannums[2]
, push1
.i = 0
:nums[0]
is greater thannums[1]
, pop1
, nowstk
is empty, push0
.
- Now,
right
is[1, 2, 5, 5, 5]
.
-
Calculating the Maximum Score:
- Initialize
ans
to0
. - Iterate over each index
i
ofnums
:- 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 isnums[1] * (right[1] - left[1] - 1)
which is1 * (2 - (-1) - 1)
equal to1
. - For
i = 2
:left[2] < k < right[2]
holds (1 < 3 < 5
), score isnums[2] * (right[2] - left[2] - 1)
which is5 * (5 - 1 - 1)
equal to15
. Updateans = 15
. - For
i = 3
:left[3] < k < right[3]
holds (-1 < 3 < 5
), score isnums[3] * (right[3] - left[3] - 1)
which is2 * (5 - (-1) - 1)
equal to8
.ans
remains15
. - For
i = 4
:left[4] < k < right[4]
doesn't hold, so skip.
- For
- Initialize
-
Return the Maximum Score:
- After iterating over all indices, we found the maximum score to be
15
, so we return15
.
- After iterating over all indices, we found the maximum score to be
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 thanO(n)
time, each element is added and removed from the stack at most once, which gives usO(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 performsO(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
andright
, each of sizen
, giving us2n
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
, andans
.
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.
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Want a Structured Path to Master System Design Too? Don’t Miss This!