1793. Maximum Score of a Good Subarray
Problem Description
You have an array of integers nums
(0-indexed) and an integer k
.
The score of a subarray from index i
to index j
is calculated as: min(nums[i], nums[i+1], ..., nums[j]) * (j - i + 1)
. In other words, the score equals the minimum element in the subarray multiplied by the length of the subarray.
A good subarray is one that contains the index k
. This means for a subarray from index i
to j
to be good, it must satisfy i ≤ k ≤ j
.
Your task is to find the maximum possible score among all good subarrays.
For example, if nums = [1,4,3,7,4,5]
and k = 3
, then:
- The subarray
[7]
(from index 3 to 3) has score7 * 1 = 7
- The subarray
[3,7,4]
(from index 2 to 4) has score3 * 3 = 9
- The subarray
[4,3,7,4,5]
(from index 1 to 5) has score3 * 5 = 15
All these subarrays contain index k = 3
, making them good subarrays. You need to return the maximum score achievable.
Intuition
The key insight is that for any subarray, its score is determined by the minimum element multiplied by the subarray length. This means that if we fix a particular element as the minimum value of a subarray, we want to extend that subarray as far as possible while maintaining that element as the minimum.
Think about it this way: if nums[i]
is going to be the minimum element of our subarray, we should expand the subarray to the left and right as much as possible while nums[i]
remains the smallest element. The farther we can extend, the larger the length becomes, and thus the larger the score.
For each element nums[i]
, we need to find:
- How far left can we extend before encountering a smaller element?
- How far right can we extend before encountering a smaller element?
This naturally leads us to use a monotonic stack approach. For each position i
, we can efficiently find:
left[i]
: the rightmost position to the left wherenums[left[i]] < nums[i]
right[i]
: the leftmost position to the right wherenums[right[i]] < nums[i]
Once we have these boundaries, the maximum subarray where nums[i]
is the minimum would span from left[i] + 1
to right[i] - 1
, giving us a score of nums[i] * (right[i] - left[i] - 1)
.
However, there's one crucial constraint: the subarray must be "good" - it must contain index k
. So when considering nums[i]
as the minimum, we can only count it if the subarray from left[i] + 1
to right[i] - 1
actually includes index k
. This means we need left[i] + 1 ≤ k ≤ right[i] - 1
.
By checking all possible minimum elements and their corresponding maximum subarrays (that include k
), we can find the maximum score.
Learn more about Stack, Two Pointers, Binary Search and Monotonic Stack patterns.
Solution Approach
The solution uses a monotonic stack to efficiently find the boundaries for each element when it serves as the minimum value of a subarray.
Step 1: Find Left Boundaries
We traverse the array from left to right with a monotonic increasing stack:
- For each element
nums[i]
, we pop elements from the stack that are greater than or equal tonums[i]
- After popping, if the stack is not empty, the top element's index is
left[i]
- the rightmost position to the left where the element is smaller thannums[i]
- If the stack is empty,
left[i] = -1
(no smaller element to the left) - Push the current index
i
onto the stack
left = [-1] * n
stk = []
for i, v in enumerate(nums):
while stk and nums[stk[-1]] >= v:
stk.pop()
if stk:
left[i] = stk[-1]
stk.append(i)
Step 2: Find Right Boundaries
We traverse the array from right to left with a similar approach:
- For each element
nums[i]
, we pop elements from the stack that are strictly greater thannums[i]
- After popping, if the stack is not empty, the top element's index is
right[i]
- the leftmost position to the right where the element is less than or equal tonums[i]
- If the stack is empty,
right[i] = n
(no smaller or equal element to the right) - Push the current index
i
onto the stack
right = [n] * n
stk = []
for i in range(n - 1, -1, -1):
v = nums[i]
while stk and nums[stk[-1]] > v:
stk.pop()
if stk:
right[i] = stk[-1]
stk.append(i)
Note the subtle difference: for the left boundary, we use >=
to pop elements, while for the right boundary, we use >
. This ensures we handle duplicate values correctly and avoid counting the same subarray multiple times.
Step 3: Calculate Maximum Score
For each element nums[i]
as the potential minimum:
- The subarray where
nums[i]
is minimum spans fromleft[i] + 1
toright[i] - 1
- Check if this subarray contains index
k
:left[i] + 1 ≤ k ≤ right[i] - 1
- If yes, calculate the score:
nums[i] * (right[i] - left[i] - 1)
- Track the maximum score
ans = 0
for i, v in enumerate(nums):
if left[i] + 1 <= k <= right[i] - 1:
ans = max(ans, v * (right[i] - left[i] - 1))
return ans
The time complexity is O(n)
as each element is pushed and popped from the stack at most once. The space complexity is O(n)
for storing the left
and right
arrays.
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 nums = [1,4,3,7,4,5]
and k = 3
.
Step 1: Find Left Boundaries
We traverse left to right with a monotonic increasing stack:
- i=0, v=1: Stack empty → left[0] = -1, push 0. Stack: [0]
- i=1, v=4: 1 < 4 → left[1] = 0, push 1. Stack: [0,1]
- i=2, v=3: Pop 1 (4≥3) → left[2] = 0, push 2. Stack: [0,2]
- i=3, v=7: 3 < 7 → left[3] = 2, push 3. Stack: [0,2,3]
- i=4, v=4: Pop 3 (7≥4) → left[4] = 2, push 4. Stack: [0,2,4]
- i=5, v=5: 4 < 5 → left[5] = 4, push 5. Stack: [0,2,4,5]
Result: left = [-1, 0, 0, 2, 2, 4]
Step 2: Find Right Boundaries
We traverse right to left with a monotonic stack:
- i=5, v=5: Stack empty → right[5] = 6, push 5. Stack: [5]
- i=4, v=4: Pop 5 (5>4) → right[4] = 6, push 4. Stack: [4]
- i=3, v=7: 4 ≤ 7 → right[3] = 4, push 3. Stack: [4,3]
- i=2, v=3: Pop 3 (7>3), Pop 4 (4>3) → right[2] = 6, push 2. Stack: [2]
- i=1, v=4: 3 ≤ 4 → right[1] = 2, push 1. Stack: [2,1]
- i=0, v=1: Pop 1 (4>1), Pop 2 (3>1) → right[0] = 6, push 0. Stack: [0]
Result: right = [6, 2, 6, 4, 6, 6]
Step 3: Calculate Maximum Score
For each element as minimum, check if subarray contains k=3:
- i=0: Subarray [0,5], contains k=3 ✓ → Score = 1 × 6 = 6
- i=1: Subarray [1,1], doesn't contain k=3 ✗
- i=2: Subarray [1,5], contains k=3 ✓ → Score = 3 × 5 = 15
- i=3: Subarray [3,3], contains k=3 ✓ → Score = 7 × 1 = 7
- i=4: Subarray [3,5], contains k=3 ✓ → Score = 4 × 3 = 12
- i=5: Subarray [5,5], doesn't contain k=3 ✗
Maximum score = 15 (when nums[2]=3 is the minimum of subarray [1,5])
Solution Implementation
1class Solution:
2 def maximumScore(self, nums: List[int], k: int) -> int:
3 n = len(nums)
4
5 # Arrays to store the nearest smaller element indices
6 # left[i]: index of nearest smaller element to the left of i (-1 if none)
7 # right[i]: index of nearest smaller element to the right of i (n if none)
8 left_smaller = [-1] * n
9 right_smaller = [n] * n
10
11 # Monotonic stack to find nearest smaller element on the left
12 stack = []
13 for i, value in enumerate(nums):
14 # Pop elements from stack that are greater than or equal to current value
15 while stack and nums[stack[-1]] >= value:
16 stack.pop()
17 # If stack is not empty, top element is the nearest smaller on the left
18 if stack:
19 left_smaller[i] = stack[-1]
20 stack.append(i)
21
22 # Clear stack for right side processing
23 stack = []
24
25 # Monotonic stack to find nearest smaller element on the right
26 for i in range(n - 1, -1, -1):
27 value = nums[i]
28 # Pop elements from stack that are greater than current value
29 # Note: using > instead of >= to handle equal elements correctly
30 while stack and nums[stack[-1]] > value:
31 stack.pop()
32 # If stack is not empty, top element is the nearest smaller on the right
33 if stack:
34 right_smaller[i] = stack[-1]
35 stack.append(i)
36
37 # Calculate maximum score
38 max_score = 0
39 for i, value in enumerate(nums):
40 # Check if k is within the subarray where nums[i] is the minimum
41 # The subarray spans from (left_smaller[i] + 1) to (right_smaller[i] - 1)
42 if left_smaller[i] + 1 <= k <= right_smaller[i] - 1:
43 # Calculate score: minimum value * subarray length
44 subarray_length = right_smaller[i] - left_smaller[i] - 1
45 score = value * subarray_length
46 max_score = max(max_score, score)
47
48 return max_score
49
1class Solution {
2 public int maximumScore(int[] nums, int k) {
3 int n = nums.length;
4
5 // Arrays to store the nearest smaller element indices
6 // left[i]: index of nearest smaller element to the left of i (-1 if none)
7 // right[i]: index of nearest smaller element to the right of i (n if none)
8 int[] left = new int[n];
9 int[] right = new int[n];
10 Arrays.fill(left, -1);
11 Arrays.fill(right, n);
12
13 // Monotonic stack to find nearest smaller elements
14 Deque<Integer> stack = new ArrayDeque<>();
15
16 // Find nearest smaller element to the left for each index
17 for (int i = 0; i < n; i++) {
18 int currentValue = nums[i];
19
20 // Pop elements from stack that are greater than or equal to current
21 while (!stack.isEmpty() && nums[stack.peek()] >= currentValue) {
22 stack.pop();
23 }
24
25 // If stack is not empty, top element is the nearest smaller to the left
26 if (!stack.isEmpty()) {
27 left[i] = stack.peek();
28 }
29
30 stack.push(i);
31 }
32
33 // Clear stack for reuse
34 stack.clear();
35
36 // Find nearest smaller element to the right for each index
37 for (int i = n - 1; i >= 0; i--) {
38 int currentValue = nums[i];
39
40 // Pop elements from stack that are greater than current
41 // Note: using > instead of >= to handle duplicates correctly
42 while (!stack.isEmpty() && nums[stack.peek()] > currentValue) {
43 stack.pop();
44 }
45
46 // If stack is not empty, top element is the nearest smaller to the right
47 if (!stack.isEmpty()) {
48 right[i] = stack.peek();
49 }
50
51 stack.push(i);
52 }
53
54 int maxScore = 0;
55
56 // Calculate maximum score for each element as the minimum in its range
57 for (int i = 0; i < n; i++) {
58 // Check if the range [left[i]+1, right[i]-1] contains index k
59 if (left[i] + 1 <= k && k <= right[i] - 1) {
60 // Calculate score: min value * range length
61 // nums[i] is the minimum in range (left[i]+1, right[i]-1)
62 int score = nums[i] * (right[i] - left[i] - 1);
63 maxScore = Math.max(maxScore, score);
64 }
65 }
66
67 return maxScore;
68 }
69}
70
1class Solution {
2public:
3 int maximumScore(vector<int>& nums, int k) {
4 int n = nums.size();
5
6 // left[i] stores the index of the nearest element to the left of i that is smaller than nums[i]
7 // -1 if no such element exists
8 vector<int> left(n, -1);
9
10 // right[i] stores the index of the nearest element to the right of i that is smaller than or equal to nums[i]
11 // n if no such element exists
12 vector<int> right(n, n);
13
14 // Monotonic stack to find the nearest smaller element on the left
15 stack<int> monoStack;
16
17 // Process from left to right to find left boundaries
18 for (int i = 0; i < n; ++i) {
19 int currentValue = nums[i];
20
21 // Pop elements from stack that are greater than or equal to current element
22 while (!monoStack.empty() && nums[monoStack.top()] >= currentValue) {
23 monoStack.pop();
24 }
25
26 // If stack is not empty, top element is the nearest smaller element on the left
27 if (!monoStack.empty()) {
28 left[i] = monoStack.top();
29 }
30
31 monoStack.push(i);
32 }
33
34 // Clear the stack for reuse
35 monoStack = stack<int>();
36
37 // Process from right to left to find right boundaries
38 for (int i = n - 1; i >= 0; --i) {
39 int currentValue = nums[i];
40
41 // Pop elements from stack that are strictly greater than current element
42 // Note: Using > instead of >= to handle duplicates correctly
43 while (!monoStack.empty() && nums[monoStack.top()] > currentValue) {
44 monoStack.pop();
45 }
46
47 // If stack is not empty, top element is the nearest smaller or equal element on the right
48 if (!monoStack.empty()) {
49 right[i] = monoStack.top();
50 }
51
52 monoStack.push(i);
53 }
54
55 int maxScore = 0;
56
57 // For each element, check if it can be the minimum in a subarray containing index k
58 for (int i = 0; i < n; ++i) {
59 // Check if the subarray [left[i] + 1, right[i] - 1] contains index k
60 if (left[i] + 1 <= k && k <= right[i] - 1) {
61 // Calculate score: minimum value * subarray length
62 int subarrayLength = right[i] - left[i] - 1;
63 int score = nums[i] * subarrayLength;
64 maxScore = max(maxScore, score);
65 }
66 }
67
68 return maxScore;
69 }
70};
71
1/**
2 * Finds the maximum score of a good subarray.
3 * A good subarray is a subarray where i <= k <= j.
4 * The score is defined as the minimum element in the subarray multiplied by the subarray's length.
5 *
6 * @param nums - The input array of numbers
7 * @param k - The index that must be included in the subarray
8 * @returns The maximum possible score
9 */
10function maximumScore(nums: number[], k: number): number {
11 const n: number = nums.length;
12
13 // leftBoundary[i] stores the index of the nearest element to the left of i that is smaller than nums[i]
14 // If no such element exists, it stores -1
15 const leftBoundary: number[] = Array(n).fill(-1);
16
17 // rightBoundary[i] stores the index of the nearest element to the right of i that is smaller than or equal to nums[i]
18 // If no such element exists, it stores n
19 const rightBoundary: number[] = Array(n).fill(n);
20
21 // Monotonic stack to track indices
22 const stack: number[] = [];
23
24 // Calculate left boundaries for each element
25 // For each position i, find the rightmost position to the left where nums[j] < nums[i]
26 for (let i = 0; i < n; ++i) {
27 // Pop elements from stack while they are greater than or equal to current element
28 while (stack.length > 0 && nums[stack[stack.length - 1]] >= nums[i]) {
29 stack.pop();
30 }
31
32 // If stack is not empty, the top element is the left boundary
33 if (stack.length > 0) {
34 leftBoundary[i] = stack[stack.length - 1];
35 }
36
37 stack.push(i);
38 }
39
40 // Clear the stack for reuse
41 stack.length = 0;
42
43 // Calculate right boundaries for each element
44 // For each position i, find the leftmost position to the right where nums[j] <= nums[i]
45 for (let i = n - 1; i >= 0; --i) {
46 // Pop elements from stack while they are greater than current element
47 while (stack.length > 0 && nums[stack[stack.length - 1]] > nums[i]) {
48 stack.pop();
49 }
50
51 // If stack is not empty, the top element is the right boundary
52 if (stack.length > 0) {
53 rightBoundary[i] = stack[stack.length - 1];
54 }
55
56 stack.push(i);
57 }
58
59 let maxScore: number = 0;
60
61 // For each element, check if it can be the minimum element in a valid subarray containing k
62 for (let i = 0; i < n; ++i) {
63 // Check if the subarray [leftBoundary[i] + 1, rightBoundary[i] - 1] contains index k
64 if (leftBoundary[i] + 1 <= k && k <= rightBoundary[i] - 1) {
65 // Calculate score: minimum element (nums[i]) * subarray length
66 const subarrayLength: number = rightBoundary[i] - leftBoundary[i] - 1;
67 const currentScore: number = nums[i] * subarrayLength;
68 maxScore = Math.max(maxScore, currentScore);
69 }
70 }
71
72 return maxScore;
73}
74
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the array nums
. This is because:
- The first for loop iterates through all
n
elements to build theleft
array. Although there's a while loop inside, each element is pushed and popped from the stack at most once, resulting inO(n)
operations total. - The second for loop iterates through all
n
elements in reverse to build theright
array. Similarly, each element is pushed and popped at most once, givingO(n)
operations. - The final for loop iterates through all
n
elements to calculate the maximum score, which takesO(n)
time. - Overall:
O(n) + O(n) + O(n) = O(n)
The space complexity is O(n)
, where n
is the length of the array nums
. This is because:
- The
left
array usesO(n)
space - The
right
array usesO(n)
space - The stack
stk
uses at mostO(n)
space in the worst case when all elements are in increasing order - Overall:
O(n) + O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Duplicate Values
The Pitfall:
One of the most common mistakes is using the same comparison operator (>=
or >
) for both left and right boundary calculations. This can lead to:
- Double counting: The same subarray might be counted multiple times when there are duplicate minimum values
- Missing subarrays: Some valid subarrays might not be considered at all
For example, with nums = [3, 3, 3]
and k = 1
:
- If we use
>=
for both boundaries, multiple elements might claim ownership of the same subarray - If we use
>
for both boundaries, we might miss valid subarrays entirely
The Solution: Use asymmetric comparison operators:
- Left boundary: Use
>=
(pop elements greater than or equal) - Right boundary: Use
>
(pop elements strictly greater)
This ensures each subarray is counted exactly once by assigning it to the leftmost occurrence of the minimum value.
2. Off-by-One Errors in Boundary Calculation
The Pitfall: Confusing the boundary indices with the actual subarray indices. The common mistakes include:
- Using
left[i]
toright[i]
as the subarray bounds directly - Incorrectly calculating the subarray length as
right[i] - left[i]
- Wrong condition check for whether
k
is in the subarray
The Solution: Remember that:
left[i]
is the index of the element strictly smaller thannums[i]
(not part of the subarray)right[i]
is the index of the element smaller or equal tonums[i]
(not part of the subarray)- The actual subarray spans from
left[i] + 1
toright[i] - 1
- The length is
(right[i] - 1) - (left[i] + 1) + 1 = right[i] - left[i] - 1
3. Not Checking if Subarray Contains k
The Pitfall:
Calculating the maximum score for all possible subarrays without verifying if they contain index k
. This violates the "good subarray" requirement.
The Solution:
Always verify left[i] + 1 <= k <= right[i] - 1
before considering the score of a subarray.
4. Stack Not Cleared Between Left and Right Processing
The Pitfall: Reusing the same stack variable without clearing it between left and right boundary calculations, leading to incorrect boundary values.
The Solution: Either:
- Explicitly clear the stack:
stack = []
orstack.clear()
- Use separate stack variables for left and right processing
What are the most two important steps in writing a depth first search function? (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
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!