Facebook Pixel

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:

  1. Look at all possible subarrays of size i + 1 in the array
  2. Find the minimum value in each of these subarrays
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 while nums[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 where nums[i] is the minimum
  • Update ans[m-1] = max(ans[m-1], nums[i]) because nums[i] could be the answer for query m-1 (subarrays of size m)

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 Evaluator

Example 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 in O(n) operations
  • Second loop to calculate right array: Similarly, each element is pushed and popped at most once, giving O(n) operations
  • Third loop to populate initial ans array: Iterates through all n elements once, taking O(n) time
  • Fourth loop to propagate maximum values backward: Iterates through n-1 elements once, taking O(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 size n: O(n) space
  • right array of size n: O(n) space
  • stk stack which can contain at most n elements: O(n) space
  • ans array of size n: 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 of window_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:

  1. Always use >= in the while condition for both left and right boundary calculations
  2. Include comprehensive test cases with duplicate elements to verify correct handling
  3. Add the propagation step and test with arrays where not all window sizes have direct minimum elements
  4. Use descriptive variable names like window_size instead of just m to make the logic clearer
  5. Add assertions or checks during development to verify boundaries make sense (e.g., left[i] < i < right[i])
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More