2832. Maximal Range That Each Element Is Maximum in It 🔒
Problem Description
You have an array nums
containing distinct integers at different positions (0-indexed). Your task is to create a new array ans
where each element represents a specific measurement related to subarrays.
For each position i
in the array, you need to find the maximum possible length of any subarray where nums[i]
is the largest element in that subarray. This means you're looking for the longest contiguous sequence of elements where nums[i]
remains the maximum value.
For example, if you have nums = [1, 5, 4, 3, 6]
:
- For
nums[0] = 1
: The longest subarray where 1 is the maximum is just[1]
itself, so length is 1 - For
nums[1] = 5
: The longest subarray where 5 is the maximum could be[1, 5, 4, 3]
, so length is 4 - For
nums[2] = 4
: The longest subarray where 4 is the maximum could be[4, 3]
, so length is 2 - And so on...
The solution uses a monotonic stack approach to efficiently find, for each element:
left[i]
: The position of the nearest element to the left that is greater thannums[i]
right[i]
: The position of the nearest element to the right that is greater thannums[i]
Once these boundaries are found, the maximum length of a subarray where nums[i]
is the maximum element is simply right[i] - left[i] - 1
. This works because any subarray containing elements between these boundaries (exclusive) will have nums[i]
as its maximum value.
The algorithm processes the array twice with a stack - once from left to right to find left boundaries, and once from right to left to find right boundaries. Elements are popped from the stack when they are smaller than or equal to the current element, ensuring the stack maintains elements in decreasing order.
Intuition
The key insight is to think about when an element nums[i]
can be the maximum of a subarray. For nums[i]
to be the maximum, all elements in that subarray must be less than or equal to nums[i]
.
This means we need to find how far we can extend to the left and right from position i
while keeping nums[i]
as the largest element. The extension stops when we encounter an element larger than nums[i]
in either direction.
So the problem transforms into finding, for each element:
- The first element greater than
nums[i]
to its left - The first element greater than
nums[i]
to its right
The region between these two boundaries (exclusive) gives us the maximum range where nums[i]
can serve as the maximum element.
Why use a monotonic stack? When we traverse the array, we want to efficiently track potential boundaries. A decreasing monotonic stack helps because:
- When we encounter a new element, any smaller elements in the stack cannot be boundaries for future elements
- The stack maintains only the "relevant" larger elements that could serve as boundaries
- Each element is pushed and popped at most once, giving us
O(n)
time complexity
For example, if we're moving left to right and maintain a decreasing stack, when we see element nums[i]
:
- We pop all elements
≤ nums[i]
from the stack (they can't be left boundaries fornums[i]
) - The top of the stack (if it exists) is the nearest larger element to the left
- We then add
nums[i]
to the stack for future elements
This approach elegantly solves what would otherwise require O(n²)
time (checking all possible subarrays) in just O(n)
time with two passes.
Learn more about Stack and Monotonic Stack patterns.
Solution Approach
The solution uses a monotonic stack pattern to find boundaries for each element. Here's the step-by-step implementation:
1. Initialize Data Structures:
- Create
left
array initialized with-1
(representing no left boundary) - Create
right
array initialized withn
(representing no right boundary) - Use an empty stack
stk
to maintain indices of elements
2. First Pass - Find Left Boundaries:
for i, x in enumerate(nums):
while stk and nums[stk[-1]] <= x:
stk.pop()
if stk:
left[i] = stk[-1]
stk.append(i)
- Traverse from left to right
- For each element
nums[i]
, pop all elements from stack that are≤ nums[i]
- After popping, the top of stack (if exists) is the index of the nearest larger element to the left
- Push current index
i
to stack for future elements
3. Second Pass - Find Right Boundaries:
stk = []
for i in range(n - 1, -1, -1):
while stk and nums[stk[-1]] <= nums[i]:
stk.pop()
if stk:
right[i] = stk[-1]
stk.append(i)
- Clear the stack and traverse from right to left
- Apply the same logic: pop elements
≤ nums[i]
- The top of stack after popping gives the nearest larger element to the right
4. Calculate Results:
return [r - l - 1 for l, r in zip(left, right)]
- For each position
i
, the maximum subarray length wherenums[i]
is the maximum isright[i] - left[i] - 1
- This formula works because:
left[i]
is the index of the first larger element to the left (exclusive boundary)right[i]
is the index of the first larger element to the right (exclusive boundary)- The valid range is
(left[i], right[i])
exclusive, so the length isright[i] - left[i] - 1
Time Complexity: O(n)
- Each element is pushed and popped at most once in each pass
Space Complexity: O(n)
- For the left
, right
arrays and stack
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 algorithm with nums = [3, 1, 5, 2]
.
Goal: For each element, find the maximum length of a subarray where that element is the largest.
Step 1: Find Left Boundaries We traverse left to right with a stack to find the nearest larger element to the left of each position.
-
i=0, nums[0]=3: Stack is empty. No larger element to the left, so
left[0] = -1
. Push index 0 to stack. Stack:[0]
-
i=1, nums[1]=1: Stack has
[0]
. Sincenums[0]=3 > 1
, don't pop. The nearest larger element to the left is at index 0, soleft[1] = 0
. Push index 1 to stack. Stack:[0, 1]
-
i=2, nums[2]=5: Stack has
[0, 1]
. Pop index 1 becausenums[1]=1 ≤ 5
. Pop index 0 becausenums[0]=3 ≤ 5
. Stack is now empty. No larger element to the left, soleft[2] = -1
. Push index 2 to stack. Stack:[2]
-
i=3, nums[3]=2: Stack has
[2]
. Sincenums[2]=5 > 2
, don't pop. The nearest larger element to the left is at index 2, soleft[3] = 2
. Push index 3 to stack. Stack:[2, 3]
Result: left = [-1, 0, -1, 2]
Step 2: Find Right Boundaries We traverse right to left with a stack to find the nearest larger element to the right of each position.
-
i=3, nums[3]=2: Stack is empty. No larger element to the right, so
right[3] = 4
(array length). Push index 3 to stack. Stack:[3]
-
i=2, nums[2]=5: Stack has
[3]
. Pop index 3 becausenums[3]=2 ≤ 5
. Stack is now empty. No larger element to the right, soright[2] = 4
. Push index 2 to stack. Stack:[2]
-
i=1, nums[1]=1: Stack has
[2]
. Sincenums[2]=5 > 1
, don't pop. The nearest larger element to the right is at index 2, soright[1] = 2
. Push index 1 to stack. Stack:[2, 1]
-
i=0, nums[0]=3: Stack has
[2, 1]
. Pop index 1 becausenums[1]=1 ≤ 3
. Stack now has[2]
. Sincenums[2]=5 > 3
, don't pop. The nearest larger element to the right is at index 2, soright[0] = 2
. Push index 0 to stack. Stack:[2, 0]
Result: right = [2, 2, 4, 4]
Step 3: Calculate Maximum Subarray Lengths
For each position i: ans[i] = right[i] - left[i] - 1
ans[0] = 2 - (-1) - 1 = 2
→ Subarray[3, 1]
has max element 3, length 2ans[1] = 2 - 0 - 1 = 1
→ Subarray[1]
has max element 1, length 1ans[2] = 4 - (-1) - 1 = 4
→ Subarray[3, 1, 5, 2]
has max element 5, length 4ans[3] = 4 - 2 - 1 = 1
→ Subarray[2]
has max element 2, length 1
Final Answer: [2, 1, 4, 1]
Each value represents the maximum length of a subarray where the corresponding element is the largest value in that subarray.
Solution Implementation
1class Solution:
2 def maximumLengthOfRanges(self, nums: List[int]) -> List[int]:
3 n = len(nums)
4
5 # left[i] stores the index of the nearest element to the left that is greater than nums[i]
6 # Initialize with -1 (no element to the left)
7 left_boundary = [-1] * n
8
9 # right[i] stores the index of the nearest element to the right that is greater than nums[i]
10 # Initialize with n (no element to the right)
11 right_boundary = [n] * n
12
13 # Monotonic decreasing stack to find left boundaries
14 stack = []
15
16 # Process each element from left to right
17 for index, value in enumerate(nums):
18 # Pop elements from stack that are less than or equal to current value
19 # These elements cannot be the left boundary for current element
20 while stack and nums[stack[-1]] <= value:
21 stack.pop()
22
23 # If stack is not empty, top element is the nearest greater element to the left
24 if stack:
25 left_boundary[index] = stack[-1]
26
27 # Add current index to stack for future elements
28 stack.append(index)
29
30 # Clear stack for right boundary calculation
31 stack = []
32
33 # Process each element from right to left
34 for index in range(n - 1, -1, -1):
35 # Pop elements from stack that are less than or equal to current value
36 # These elements cannot be the right boundary for current element
37 while stack and nums[stack[-1]] <= nums[index]:
38 stack.pop()
39
40 # If stack is not empty, top element is the nearest greater element to the right
41 if stack:
42 right_boundary[index] = stack[-1]
43
44 # Add current index to stack for future elements
45 stack.append(index)
46
47 # Calculate the range length for each element
48 # Range extends from (left_boundary[i] + 1) to (right_boundary[i] - 1)
49 # Length = right_boundary[i] - left_boundary[i] - 1
50 return [right - left - 1 for left, right in zip(left_boundary, right_boundary)]
51
1class Solution {
2 public int[] maximumLengthOfRanges(int[] nums) {
3 int n = nums.length;
4
5 // left[i] stores the index of the nearest element to the left of i that is greater than nums[i]
6 // -1 if no such element exists
7 int[] left = new int[n];
8
9 // right[i] stores the index of the nearest element to the right of i that is greater than nums[i]
10 // n if no such element exists
11 int[] right = new int[n];
12
13 // Initialize arrays with default boundary values
14 Arrays.fill(left, -1);
15 Arrays.fill(right, n);
16
17 // Stack to maintain indices in monotonic decreasing order of their values
18 Deque<Integer> stack = new ArrayDeque<>();
19
20 // Find the nearest greater element to the left for each position
21 for (int i = 0; i < n; i++) {
22 // Pop elements from stack while they are less than or equal to current element
23 while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
24 stack.pop();
25 }
26
27 // If stack is not empty, top element is the nearest greater element to the left
28 if (!stack.isEmpty()) {
29 left[i] = stack.peek();
30 }
31
32 // Push current index to stack
33 stack.push(i);
34 }
35
36 // Clear stack for reuse
37 stack.clear();
38
39 // Find the nearest greater element to the right for each position
40 for (int i = n - 1; i >= 0; i--) {
41 // Pop elements from stack while they are less than or equal to current element
42 while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
43 stack.pop();
44 }
45
46 // If stack is not empty, top element is the nearest greater element to the right
47 if (!stack.isEmpty()) {
48 right[i] = stack.peek();
49 }
50
51 // Push current index to stack
52 stack.push(i);
53 }
54
55 // Calculate the maximum length of range where nums[i] is the maximum element
56 int[] result = new int[n];
57 for (int i = 0; i < n; i++) {
58 // Range extends from (left[i] + 1) to (right[i] - 1), inclusive
59 result[i] = right[i] - left[i] - 1;
60 }
61
62 return result;
63 }
64}
65
1class Solution {
2public:
3 vector<int> maximumLengthOfRanges(vector<int>& nums) {
4 int n = nums.size();
5
6 // left[i] stores the index of the nearest element to the left that is greater than nums[i]
7 // Initialize with -1 (no element found)
8 vector<int> left(n, -1);
9
10 // right[i] stores the index of the nearest element to the right that is greater than nums[i]
11 // Initialize with n (no element found)
12 vector<int> right(n, n);
13
14 // Monotonic stack to find previous greater element
15 stack<int> stk;
16
17 // Find the previous greater element for each position
18 for (int i = 0; i < n; ++i) {
19 // Pop elements from stack that are less than or equal to current element
20 while (!stk.empty() && nums[stk.top()] <= nums[i]) {
21 stk.pop();
22 }
23
24 // If stack is not empty, top element is the previous greater element
25 if (!stk.empty()) {
26 left[i] = stk.top();
27 }
28
29 // Push current index to stack
30 stk.push(i);
31 }
32
33 // Clear the stack for reuse
34 stk = stack<int>();
35
36 // Find the next greater element for each position
37 for (int i = n - 1; i >= 0; --i) {
38 // Pop elements from stack that are less than or equal to current element
39 while (!stk.empty() && nums[stk.top()] <= nums[i]) {
40 stk.pop();
41 }
42
43 // If stack is not empty, top element is the next greater element
44 if (!stk.empty()) {
45 right[i] = stk.top();
46 }
47
48 // Push current index to stack
49 stk.push(i);
50 }
51
52 // Calculate the maximum length of range for each element
53 vector<int> ans(n);
54 for (int i = 0; i < n; ++i) {
55 // The range where nums[i] is the maximum extends from (left[i] + 1) to (right[i] - 1)
56 // Length = right[i] - left[i] - 1
57 ans[i] = right[i] - left[i] - 1;
58 }
59
60 return ans;
61 }
62};
63
1/**
2 * Calculates the maximum length of ranges where each element is the maximum
3 * @param nums - The input array of numbers
4 * @returns An array where each element represents the maximum range length for that position
5 */
6function maximumLengthOfRanges(nums: number[]): number[] {
7 const n: number = nums.length;
8
9 // left[i] stores the index of the nearest element to the left that is greater than nums[i]
10 // Initialize with -1 (no element to the left is greater)
11 const left: number[] = Array(n).fill(-1);
12
13 // right[i] stores the index of the nearest element to the right that is greater than nums[i]
14 // Initialize with n (no element to the right is greater)
15 const right: number[] = Array(n).fill(n);
16
17 // Monotonic stack to track indices
18 const stack: number[] = [];
19
20 // Find the nearest greater element to the left for each position
21 for (let i = 0; i < n; ++i) {
22 // Pop elements from stack while they are less than or equal to current element
23 while (stack.length > 0 && nums[stack[stack.length - 1]] <= nums[i]) {
24 stack.pop();
25 }
26
27 // If stack is not empty, the top element is the nearest greater element to the left
28 if (stack.length > 0) {
29 left[i] = stack[stack.length - 1];
30 }
31
32 // Add current index to stack
33 stack.push(i);
34 }
35
36 // Clear the stack for reuse
37 stack.length = 0;
38
39 // Find the nearest greater element to the right for each position
40 for (let i = n - 1; i >= 0; --i) {
41 // Pop elements from stack while they are less than or equal to current element
42 while (stack.length > 0 && nums[stack[stack.length - 1]] <= nums[i]) {
43 stack.pop();
44 }
45
46 // If stack is not empty, the top element is the nearest greater element to the right
47 if (stack.length > 0) {
48 right[i] = stack[stack.length - 1];
49 }
50
51 // Add current index to stack
52 stack.push(i);
53 }
54
55 // Calculate the range length for each position
56 // Range extends from (left[i] + 1) to (right[i] - 1), so length is right[i] - left[i] - 1
57 return left.map((leftBound: number, i: number) => right[i] - leftBound - 1);
58}
59
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the input array nums
. Although there are two for loops that iterate through the array and within each loop there's a while loop for stack operations, each element is pushed and popped from the stack at most once. The first loop processes elements from left to right to find the nearest greater element on the left, and the second loop processes elements from right to left to find the nearest greater element on the right. Therefore, the total number of operations is linear with respect to n
.
The space complexity is O(n)
. The algorithm uses three arrays (left
, right
, and the result array from the list comprehension) each of size n
, and a stack stk
that can contain at most n
elements. Even though the stack is reused between the two loops, the overall space requirement is still O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Equal Elements
One of the most common mistakes is using strict inequality (<
) instead of <=
when popping elements from the stack.
Wrong Implementation:
while stack and nums[stack[-1]] < value: # Wrong: using < instead of <= stack.pop()
Why this fails: If we only pop elements strictly less than the current value, equal elements would remain in the stack. This would incorrectly limit the range where the current element can be the maximum. For example, with nums = [3, 3, 3]
, each element should have a range length of 1 (only itself), but using <
would give incorrect boundaries.
Correct Implementation:
while stack and nums[stack[-1]] <= value: # Correct: using <= stack.pop()
2. Forgetting to Clear/Reset the Stack Between Passes
Another common mistake is reusing the same stack for both left and right boundary calculations without clearing it.
Wrong Implementation:
stack = []
# First pass for left boundaries
for i, x in enumerate(nums):
# ... process left boundaries
# Forgot to clear stack!
# stack = [] # Missing this line
for i in range(n - 1, -1, -1):
# ... process right boundaries with contaminated stack
Solution: Always reset the stack before the second pass:
stack = [] # Clear stack before right boundary calculation
3. Incorrect Boundary Initialization
Initializing boundaries incorrectly can lead to wrong range calculations.
Wrong Implementation:
left_boundary = [0] * n # Wrong: should be -1 right_boundary = [n - 1] * n # Wrong: should be n
Why this fails:
- If an element has no greater element to its left, its left boundary should be -1 (before index 0)
- If an element has no greater element to its right, its right boundary should be n (after index n-1)
- Using 0 and n-1 would incorrectly include actual array positions as boundaries
Correct Implementation:
left_boundary = [-1] * n # Correct: -1 for no left boundary right_boundary = [n] * n # Correct: n for no right boundary
4. Off-by-One Error in Range Calculation
A subtle but critical error is forgetting the -1
in the final calculation.
Wrong Implementation:
return [right - left for left, right in zip(left_boundary, right_boundary)] # Missing -1
Why this fails: The boundaries are exclusive (they point to elements greater than the current element), so the actual range is from left + 1
to right - 1
inclusive. The length is therefore (right - 1) - (left + 1) + 1 = right - left - 1
.
Correct Implementation:
return [right - left - 1 for left, right in zip(left_boundary, right_boundary)]
Which technique can we use to find the middle of a linked list?
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
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!