1950. Maximum of Minimum Values in All Subarrays 🔒
Problem Description
You have an integer array nums
of size n
. You need to answer n
different queries, where each query i
(for 0 <= i < n
) asks you to:
- Look at all possible subarrays of size
i + 1
in the array - Find the minimum value in each of these subarrays
- Return the maximum among all these minimum values
The final answer should be an array ans
of size n
, where ans[i]
contains the answer to the i
-th query.
For example, if nums = [1, 3, 2, 4]
:
- Query 0 (subarray size 1): All subarrays are
[1]
,[3]
,[2]
,[4]
. Their minimums are 1, 3, 2, 4. The maximum of these is 4. - Query 1 (subarray size 2): All subarrays are
[1,3]
,[3,2]
,[2,4]
. Their minimums are 1, 2, 2. The maximum of these is 2. - Query 2 (subarray size 3): All subarrays are
[1,3,2]
,[3,2,4]
. Their minimums are 1, 2. The maximum of these is 2. - Query 3 (subarray size 4): Only one subarray
[1,3,2,4]
. Its minimum is 1. The maximum is 1.
So the answer would be [4, 2, 2, 1]
.
The solution uses a monotonic stack approach to efficiently find, for each element, the largest subarray where that element is the minimum. The left[i]
and right[i]
arrays store the boundaries of the largest window where nums[i]
is the minimum element. The distance right[i] - left[i] - 1
gives the maximum window size where nums[i]
can be the minimum, which helps determine which query answers should consider this element.
Intuition
The key insight is to reverse the problem's perspective. Instead of finding minimums for each subarray size, we can think about: for each element in the array, what's the largest subarray where this element is the minimum?
Consider any element nums[i]
. If we know the largest subarray where nums[i]
is the minimum element, we can determine which query answers this element contributes to. For instance, if nums[i]
is the minimum in a subarray of maximum length m
, then it could potentially be the answer for query m-1
(subarrays of size m
).
To find the largest subarray where each element is the minimum, we need to find:
- The nearest smaller element to the left (or -1 if none exists)
- The nearest smaller element to the right (or n if none exists)
The region between these boundaries is where nums[i]
can be the minimum. We use monotonic stacks for this - maintaining an increasing stack helps us efficiently find these boundaries in O(n) time.
Once we know that nums[i]
can be the minimum for subarrays up to size m
, we update ans[m-1] = max(ans[m-1], nums[i])
. This is because for query m-1
(subarrays of size m
), nums[i]
is one possible minimum value we need to consider.
The final trick is recognizing that if an element is the answer for subarrays of size k
, it's also a valid candidate for all smaller subarray sizes. So we propagate the maximum values backwards: ans[i] = max(ans[i], ans[i+1])
. This ensures that each query gets the best possible answer from all applicable elements.
Learn more about Stack and Monotonic Stack patterns.
Solution Approach
The implementation uses monotonic stacks to find the boundaries for each element where it serves as the minimum value.
Step 1: Find Left Boundaries
We iterate through the array from left to right with a stack. For each element nums[i]
:
- Pop elements from the stack while
nums[stk[-1]] >= nums[i]
(remove elements that are greater than or equal to current) - The top of the stack (if exists) gives us
left[i]
- the index of the nearest smaller element to the left - Push current index
i
onto the stack
This maintains a strictly increasing stack, ensuring we always find the nearest smaller element.
Step 2: Find Right Boundaries Similar process but iterating from right to left:
- For each position
i
, pop elements whilenums[stk[-1]] >= nums[i]
- The top of the stack (if exists) gives us
right[i]
- the index of the nearest smaller element to the right - Push current index
i
onto the stack
Step 3: Calculate Maximum Window Sizes
For each element at position i
:
- Calculate
m = right[i] - left[i] - 1
, which is the maximum subarray length wherenums[i]
is the minimum - Update
ans[m-1] = max(ans[m-1], nums[i])
becausenums[i]
could be the answer for querym-1
(subarrays of sizem
)
Step 4: Propagate Maximum Values Since larger elements can also be candidates for smaller subarray sizes, we propagate backwards:
for i in range(n - 2, -1, -1):
ans[i] = max(ans[i], ans[i + 1])
This ensures that if an element is the best answer for size k
, it's also considered for all sizes less than k
.
Time Complexity: O(n) - each element is pushed and popped from the stack at most once
Space Complexity: O(n) - for the left
, right
, and stack 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 = [3, 1, 5, 4]
.
Step 1: Find Left Boundaries
- i=0, nums[0]=3: Stack is empty. left[0] = -1. Stack: [0]
- i=1, nums[1]=1: Pop 0 (3≥1). Stack empty. left[1] = -1. Stack: [1]
- i=2, nums[2]=5: nums[1]=1 < 5. left[2] = 1. Stack: [1, 2]
- i=3, nums[3]=4: Pop 2 (5≥4). nums[1]=1 < 4. left[3] = 1. Stack: [1, 3]
Result: left = [-1, -1, 1, 1]
Step 2: Find Right Boundaries
- i=3, nums[3]=4: Stack is empty. right[3] = 4. Stack: [3]
- i=2, nums[2]=5: nums[3]=4 < 5. right[2] = 3. Stack: [3, 2]
- i=1, nums[1]=1: Pop 2 (5≥1), pop 3 (4≥1). Stack empty. right[1] = 4. Stack: [1]
- i=0, nums[0]=3: nums[1]=1 < 3. right[0] = 1. Stack: [1, 0]
Result: right = [1, 4, 3, 4]
Step 3: Calculate Maximum Window Sizes and Update ans Initialize: ans = [0, 0, 0, 0]
- i=0, nums[0]=3: window size = 1 - (-1) - 1 = 1 Update ans[0] = max(0, 3) = 3
- i=1, nums[1]=1: window size = 4 - (-1) - 1 = 4 Update ans[3] = max(0, 1) = 1
- i=2, nums[2]=5: window size = 3 - 1 - 1 = 1 Update ans[0] = max(3, 5) = 5
- i=3, nums[3]=4: window size = 4 - 1 - 1 = 2 Update ans[1] = max(0, 4) = 4
After Step 3: ans = [5, 4, 0, 1]
Step 4: Propagate Maximum Values Backwards
- i=2: ans[2] = max(0, 1) = 1
- i=1: ans[1] = max(4, 1) = 4
- i=0: ans[0] = max(5, 4) = 5
Final Result: ans = [5, 4, 1, 1]
Verification:
- Query 0 (size 1): Subarrays [3], [1], [5], [4] → mins: 3, 1, 5, 4 → max = 5 ✓
- Query 1 (size 2): Subarrays [3,1], [1,5], [5,4] → mins: 1, 1, 4 → max = 4 ✓
- Query 2 (size 3): Subarrays [3,1,5], [1,5,4] → mins: 1, 1 → max = 1 ✓
- Query 3 (size 4): Subarray [3,1,5,4] → min: 1 → max = 1 ✓
Solution Implementation
1class Solution:
2 def findMaximums(self, nums: List[int]) -> List[int]:
3 """
4 Find the maximum value among all subarrays of each possible size.
5 For each window size k (1 to n), find the maximum of minimums of all subarrays of size k.
6
7 Args:
8 nums: List of integers
9
10 Returns:
11 List where ans[i] is the maximum of minimums of all subarrays of size i+1
12 """
13 n = len(nums)
14
15 # For each element, find the nearest smaller element on the left
16 # left[i] = index of nearest smaller element to the left of i (-1 if none)
17 left_boundaries = [-1] * n
18 stack = []
19
20 for i, num in enumerate(nums):
21 # Pop elements from stack that are >= current element
22 while stack and nums[stack[-1]] >= num:
23 stack.pop()
24
25 # If stack has elements, top is the nearest smaller on left
26 if stack:
27 left_boundaries[i] = stack[-1]
28
29 stack.append(i)
30
31 # For each element, find the nearest smaller element on the right
32 # right[i] = index of nearest smaller element to the right of i (n if none)
33 right_boundaries = [n] * n
34 stack = []
35
36 for i in range(n - 1, -1, -1):
37 # Pop elements from stack that are >= current element
38 while stack and nums[stack[-1]] >= nums[i]:
39 stack.pop()
40
41 # If stack has elements, top is the nearest smaller on right
42 if stack:
43 right_boundaries[i] = stack[-1]
44
45 stack.append(i)
46
47 # Initialize result array with zeros
48 result = [0] * n
49
50 # For each element, calculate the maximum window size where it's the minimum
51 for i in range(n):
52 # Calculate window size where nums[i] is the minimum
53 # This is the distance between left and right boundaries minus 1
54 window_size = right_boundaries[i] - left_boundaries[i] - 1
55
56 # Update the maximum for this window size
57 # Note: window_size - 1 because array is 0-indexed
58 result[window_size - 1] = max(result[window_size - 1], nums[i])
59
60 # Fill gaps by propagating maximum values from larger windows to smaller ones
61 # If no element is minimum for window size k, use the value from window size k+1
62 for i in range(n - 2, -1, -1):
63 result[i] = max(result[i], result[i + 1])
64
65 return result
66
1class Solution {
2 public int[] findMaximums(int[] nums) {
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 // Stack to maintain indices in monotonic increasing order of values
14 Deque<Integer> stack = new ArrayDeque<>();
15
16 // Find nearest smaller element to the left for each position
17 for (int i = 0; i < n; i++) {
18 // Pop elements from stack that are greater than or equal to current element
19 while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
20 stack.pop();
21 }
22 // If stack is not empty, top element is the nearest smaller to the left
23 if (!stack.isEmpty()) {
24 left[i] = stack.peek();
25 }
26 // Push current index to stack
27 stack.push(i);
28 }
29
30 // Clear stack for reuse
31 stack.clear();
32
33 // Find nearest smaller element to the right for each position
34 for (int i = n - 1; i >= 0; i--) {
35 // Pop elements from stack that are greater than or equal to current element
36 while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
37 stack.pop();
38 }
39 // If stack is not empty, top element is the nearest smaller to the right
40 if (!stack.isEmpty()) {
41 right[i] = stack.peek();
42 }
43 // Push current index to stack
44 stack.push(i);
45 }
46
47 // Initialize result array
48 int[] result = new int[n];
49
50 // For each element, calculate the window size where it's the minimum
51 // and update the maximum for that window size
52 for (int i = 0; i < n; i++) {
53 // Calculate window size where nums[i] is the minimum
54 int windowSize = right[i] - left[i] - 1;
55 // Update maximum for this window size (0-indexed, so windowSize - 1)
56 result[windowSize - 1] = Math.max(result[windowSize - 1], nums[i]);
57 }
58
59 // Propagate maximums from larger windows to smaller windows
60 // If no element is minimum for window size k, use the maximum from window size k+1
61 for (int i = n - 2; i >= 0; i--) {
62 result[i] = Math.max(result[i], result[i + 1]);
63 }
64
65 return result;
66 }
67}
68
1class Solution {
2public:
3 vector<int> findMaximums(vector<int>& nums) {
4 int n = nums.size();
5
6 // For each element, store the index of the nearest smaller element to its left
7 // -1 means no smaller element exists to the left
8 vector<int> leftBoundary(n, -1);
9
10 // For each element, store the index of the nearest smaller element to its right
11 // n means no smaller element exists to the right
12 vector<int> rightBoundary(n, n);
13
14 // Monotonic stack to find nearest smaller elements on the left
15 stack<int> monoStack;
16
17 // Find the nearest smaller element to the left for each element
18 for (int i = 0; i < n; ++i) {
19 // Pop elements from stack that are greater than or equal to current element
20 while (!monoStack.empty() && nums[monoStack.top()] >= nums[i]) {
21 monoStack.pop();
22 }
23
24 // If stack is not empty, top element is the nearest smaller element to the left
25 if (!monoStack.empty()) {
26 leftBoundary[i] = monoStack.top();
27 }
28
29 // Push current index to stack for future elements
30 monoStack.push(i);
31 }
32
33 // Clear the stack for reuse
34 monoStack = stack<int>();
35
36 // Find the nearest smaller element to the right for each element
37 for (int i = n - 1; i >= 0; --i) {
38 // Pop elements from stack that are greater than or equal to current element
39 while (!monoStack.empty() && nums[monoStack.top()] >= nums[i]) {
40 monoStack.pop();
41 }
42
43 // If stack is not empty, top element is the nearest smaller element to the right
44 if (!monoStack.empty()) {
45 rightBoundary[i] = monoStack.top();
46 }
47
48 // Push current index to stack for future elements
49 monoStack.push(i);
50 }
51
52 // Result array where result[i] represents the maximum of minimums for all windows of size i+1
53 vector<int> result(n);
54
55 // For each element, calculate the maximum window size where it can be the minimum
56 for (int i = 0; i < n; ++i) {
57 // Calculate the window size where nums[i] is the minimum
58 // This is the distance between the nearest smaller elements on both sides
59 int windowSize = rightBoundary[i] - leftBoundary[i] - 1;
60
61 // Update the result for this window size (0-indexed, so windowSize - 1)
62 result[windowSize - 1] = max(result[windowSize - 1], nums[i]);
63 }
64
65 // Propagate maximum values from larger windows to smaller windows
66 // If an element is the minimum in a window of size k, it can also be
67 // the minimum in windows of smaller sizes
68 for (int i = n - 2; i >= 0; --i) {
69 result[i] = max(result[i], result[i + 1]);
70 }
71
72 return result;
73 }
74};
75
1function findMaximums(nums: number[]): number[] {
2 const n = nums.length;
3
4 // For each element, store the index of the nearest smaller element to its left
5 // -1 means no smaller element exists to the left
6 const leftBoundary: number[] = new Array(n).fill(-1);
7
8 // For each element, store the index of the nearest smaller element to its right
9 // n means no smaller element exists to the right
10 const rightBoundary: number[] = new Array(n).fill(n);
11
12 // Monotonic stack to find nearest smaller elements on the left
13 let monoStack: number[] = [];
14
15 // Find the nearest smaller element to the left for each element
16 for (let i = 0; i < n; i++) {
17 // Pop elements from stack that are greater than or equal to current element
18 while (monoStack.length > 0 && nums[monoStack[monoStack.length - 1]] >= nums[i]) {
19 monoStack.pop();
20 }
21
22 // If stack is not empty, top element is the nearest smaller element to the left
23 if (monoStack.length > 0) {
24 leftBoundary[i] = monoStack[monoStack.length - 1];
25 }
26
27 // Push current index to stack for future elements
28 monoStack.push(i);
29 }
30
31 // Clear the stack for reuse
32 monoStack = [];
33
34 // Find the nearest smaller element to the right for each element
35 for (let i = n - 1; i >= 0; i--) {
36 // Pop elements from stack that are greater than or equal to current element
37 while (monoStack.length > 0 && nums[monoStack[monoStack.length - 1]] >= nums[i]) {
38 monoStack.pop();
39 }
40
41 // If stack is not empty, top element is the nearest smaller element to the right
42 if (monoStack.length > 0) {
43 rightBoundary[i] = monoStack[monoStack.length - 1];
44 }
45
46 // Push current index to stack for future elements
47 monoStack.push(i);
48 }
49
50 // Result array where result[i] represents the maximum of minimums for all windows of size i+1
51 const result: number[] = new Array(n).fill(0);
52
53 // For each element, calculate the maximum window size where it can be the minimum
54 for (let i = 0; i < n; i++) {
55 // Calculate the window size where nums[i] is the minimum
56 // This is the distance between the nearest smaller elements on both sides
57 const windowSize = rightBoundary[i] - leftBoundary[i] - 1;
58
59 // Update the result for this window size (0-indexed, so windowSize - 1)
60 result[windowSize - 1] = Math.max(result[windowSize - 1], nums[i]);
61 }
62
63 // Propagate maximum values from larger windows to smaller windows
64 // If an element is the minimum in a window of size k, it can also be
65 // the minimum in windows of smaller sizes
66 for (let i = n - 2; i >= 0; i--) {
67 result[i] = Math.max(result[i], result[i + 1]);
68 }
69
70 return result;
71}
72
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of several linear passes through the array:
- First loop to calculate
left
array: Each element is pushed and popped from the stack at most once, resulting inO(n)
operations - Second loop to calculate
right
array: Similarly, each element is pushed and popped at most once, givingO(n)
operations - Third loop to populate initial
ans
array: Iterates through alln
elements once, takingO(n)
time - Fourth loop to propagate maximum values backward: Iterates through
n-1
elements once, takingO(n)
time
Total time complexity: O(n) + O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
The algorithm uses the following additional space:
left
array of sizen
:O(n)
spaceright
array of sizen
:O(n)
spacestk
stack which can contain at mostn
elements:O(n)
spaceans
array of sizen
:O(n)
space (this is the output array, often not counted in auxiliary space)
If we count only auxiliary space (excluding the output): O(n)
If we include the output array: O(n)
Total space complexity: O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Equal Elements
The most critical pitfall in this solution is how we handle equal elements when building the monotonic stack. The condition nums[stack[-1]] >= num
(using >=
instead of >
) is essential for correctness.
Why this matters: When multiple equal elements exist, we need to ensure each element considers the correct window boundaries. Using >=
ensures that when we have duplicate values, each instance gets its proper left boundary.
Example of the bug:
# INCORRECT: Using only > instead of >= while stack and nums[stack[-1]] > num: # Bug! stack.pop()
Consider nums = [2, 2, 2]
:
- With
>
: Each 2 would consider all other 2s as part of its window, leading to incorrect window sizes - With
>=
: Each 2 gets properly bounded, ensuring correct window size calculations
2. Forgetting the Propagation Step
Another common mistake is omitting the backward propagation step:
# This step is ESSENTIAL - don't forget it!
for i in range(n - 2, -1, -1):
result[i] = max(result[i], result[i + 1])
Why this matters: Not all window sizes might have a direct element that serves as the minimum for exactly that size. The propagation ensures that if an element can be the answer for a larger window, it's also considered for smaller windows.
Example scenario:
If nums = [5, 3, 1, 4]
, element 3 might be the best answer for windows of size 2, but there might not be any element that's the minimum for exactly size 1 subarrays. The propagation ensures the answer for size 1 considers element 3 as well.
3. Off-by-One Errors in Window Size Calculation
The window size calculation and array indexing can be confusing:
window_size = right_boundaries[i] - left_boundaries[i] - 1
result[window_size - 1] = max(result[window_size - 1], nums[i])
Common mistakes:
- Forgetting the
-1
when calculating window size - Forgetting the
-1
when indexing into the result array (window size k corresponds to index k-1) - Using
window_size
directly as the index instead ofwindow_size - 1
4. Stack Initialization Between Left and Right Passes
Forgetting to reset the stack between computing left and right boundaries:
# After computing left boundaries stack = [] # Don't forget to reset! # Before computing right boundaries
Using the same stack without clearing it would produce completely incorrect boundaries for the right side.
Solution to Avoid These Pitfalls:
- Always use
>=
in the while condition for both left and right boundary calculations - Include comprehensive test cases with duplicate elements to verify correct handling
- Add the propagation step and test with arrays where not all window sizes have direct minimum elements
- Use descriptive variable names like
window_size
instead of justm
to make the logic clearer - Add assertions or checks during development to verify boundaries make sense (e.g.,
left[i] < i < right[i]
)
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
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!