LeetCode 1950 - Maximum of Minimum Values in All Subarrays
This problem asks us to compute, for every possible subarray size, the best possible minimum value among all subarrays of that size. Given an array nums of length n, we must evaluate every window size from 1 to n. For each size k, we consider all contiguous subarrays of length k.
Difficulty: 🟡 Medium
Topics: Array, Stack, Monotonic Stack
Solution
Problem Understanding
This problem asks us to compute, for every possible subarray size, the best possible minimum value among all subarrays of that size.
Given an array nums of length n, we must evaluate every window size from 1 to n. For each size k, we consider all contiguous subarrays of length k. Every such subarray has a minimum element. Among all those minimum values, we select the maximum one.
The output array ans has length n, where:
ans[0]corresponds to subarrays of size1ans[1]corresponds to subarrays of size2ans[i]corresponds to subarrays of sizei + 1
The key difficulty is that the constraints are large:
ncan be up to100000- A naive nested-window solution becomes too slow
A brute-force approach that explicitly computes all subarrays would require quadratic or cubic work, which is not feasible at this scale.
The important observation is that each element can act as the minimum for a range of subarrays. Instead of iterating over every subarray, we can determine the largest window size for which a particular element remains the minimum. This transforms the problem into a monotonic stack problem.
Several edge cases are important:
- Arrays with all equal values
- Strictly increasing arrays
- Strictly decreasing arrays
- Arrays containing duplicate minima
- Arrays of size
1
Handling duplicates correctly is especially important because incorrect inequality choices in the monotonic stack can produce overlapping ranges or missed windows.
Approaches
Brute Force Approach
The most direct solution is to evaluate every possible subarray.
For every window size k:
- Enumerate all subarrays of length
k - Compute the minimum value in each subarray
- Track the maximum among those minimums
For example, with nums = [0,1,2,4] and k = 2:
[0,1] -> min = 0[1,2] -> min = 1[2,4] -> min = 2
The answer for size 2 is 2.
This method is correct because it explicitly checks every required subarray. However, it is far too slow.
If we compute minima naively, then:
- There are
O(n)window sizes - Each size has
O(n)subarrays - Each minimum computation costs
O(n)
Total complexity becomes O(n^3).
Even with sliding window optimizations, achieving all answers separately still leads to excessive work.
Optimal Approach
The optimal solution uses a monotonic stack.
The central insight is this:
Instead of asking:
"What is the minimum for every subarray?"
we ask:
"For which subarrays is a given element the minimum?"
For each element nums[i], we determine:
- The nearest smaller element on the left
- The nearest smaller element on the right
These boundaries tell us the largest window in which nums[i] remains the minimum value.
If:
left[i]is the index of the previous smaller elementright[i]is the index of the next smaller element
then nums[i] is the minimum for all windows inside:
(left[i], right[i])
The maximum window length is:
right[i] - left[i] - 1
This means:
nums[i]can serve as the minimum for a window of that size- Therefore it is a candidate answer for that window length
We compute these ranges in linear time using monotonic stacks.
After filling candidate answers, we propagate values backward because if a value works for a larger window, it also constrains smaller windows.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^3) | O(1) | Explicitly checks all subarrays |
| Optimal | O(n) | O(n) | Uses monotonic stacks to compute contribution ranges |
Algorithm Walkthrough
Step 1: Compute previous smaller elements
We maintain an increasing monotonic stack.
For each index i:
- Pop elements while the top of the stack is greater than or equal to
nums[i] - The remaining top becomes the previous smaller element
- If the stack is empty, use
-1
This gives the left boundary where nums[i] stops being the minimum.
Step 2: Compute next smaller elements
We repeat the process from right to left.
For each index i:
- Pop elements while the top is greater than or equal to
nums[i] - The remaining top becomes the next smaller element
- If none exists, use
n
This gives the right boundary.
Step 3: Determine the maximum window size for each element
For every index i:
window_size = right[i] - left[i] - 1
This is the largest subarray length where nums[i] is the minimum.
We update:
ans[window_size - 1] = max(ans[window_size - 1], nums[i])
because we want the maximum possible minimum value for each window size.
Step 4: Fill missing entries
Some window sizes may remain unset.
Example:
[50, 0, 10, 0]
The answers must be non-increasing as window size grows.
If a value works for a larger window, it also constrains smaller windows.
So we process from right to left:
ans[i] = max(ans[i], ans[i + 1])
This fills all missing values correctly.
Why it works
For every element, the monotonic stack identifies the largest contiguous range where that element is the smallest value. Any subarray entirely inside that range has that element as its minimum.
Thus, every element contributes exactly to the window sizes where it can serve as the minimum. Taking the maximum contribution for each window size guarantees the correct answer.
The backward propagation step works because the answer sequence is monotonic:
ans[i] >= ans[i + 1]
Larger windows cannot have larger maximum minimums than smaller windows.
Python Solution
from typing import List
class Solution:
def findMaximums(self, nums: List[int]) -> List[int]:
n = len(nums)
left = [-1] * n
right = [n] * n
stack = []
# Previous smaller element
for i in range(n):
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
if stack:
left[i] = stack[-1]
stack.append(i)
stack.clear()
# Next smaller element
for i in range(n - 1, -1, -1):
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
if stack:
right[i] = stack[-1]
stack.append(i)
ans = [0] * n
# Assign values to window sizes
for i in range(n):
window_size = right[i] - left[i] - 1
ans[window_size - 1] = max(
ans[window_size - 1],
nums[i]
)
# Fill missing values
for i in range(n - 2, -1, -1):
ans[i] = max(ans[i], ans[i + 1])
return ans
The implementation begins by computing the nearest smaller elements on both sides of every position. The monotonic stack ensures that every element is pushed and popped at most once, giving linear complexity.
The left array stores the index of the previous smaller element. The right array stores the next smaller element.
Once those boundaries are known, the code computes the largest window where each element remains the minimum. That window size determines which answer slot the element contributes to.
Finally, the backward pass fills gaps and enforces the monotonic property of the answers.
Go Solution
func findMaximums(nums []int) []int {
n := len(nums)
left := make([]int, n)
right := make([]int, n)
for i := 0; i < n; i++ {
left[i] = -1
right[i] = n
}
stack := []int{}
// Previous smaller element
for i := 0; i < n; i++ {
for len(stack) > 0 && nums[stack[len(stack)-1]] >= nums[i] {
stack = stack[:len(stack)-1]
}
if len(stack) > 0 {
left[i] = stack[len(stack)-1]
}
stack = append(stack, i)
}
stack = []int{}
// Next smaller element
for i := n - 1; i >= 0; i-- {
for len(stack) > 0 && nums[stack[len(stack)-1]] >= nums[i] {
stack = stack[:len(stack)-1]
}
if len(stack) > 0 {
right[i] = stack[len(stack)-1]
}
stack = append(stack, i)
}
ans := make([]int, n)
// Assign values to window sizes
for i := 0; i < n; i++ {
windowSize := right[i] - left[i] - 1
if nums[i] > ans[windowSize-1] {
ans[windowSize-1] = nums[i]
}
}
// Fill missing values
for i := n - 2; i >= 0; i-- {
if ans[i+1] > ans[i] {
ans[i] = ans[i+1]
}
}
return ans
}
The Go implementation follows the same algorithmic structure as the Python version.
Slices are used for stacks because appending and truncating are efficient. Integer overflow is not an issue because values are bounded by 10^9, which safely fits inside Go's int on standard LeetCode environments.
The arrays are initialized with sentinel values:
-1for missing left boundariesnfor missing right boundaries
This simplifies window length computation.
Worked Examples
Example 1
nums = [0,1,2,4]
Previous Smaller Array
| Index | Value | Previous Smaller Index |
|---|---|---|
| 0 | 0 | -1 |
| 1 | 1 | 0 |
| 2 | 2 | 1 |
| 3 | 4 | 2 |
Next Smaller Array
| Index | Value | Next Smaller Index |
|---|---|---|
| 0 | 0 | 4 |
| 1 | 1 | 4 |
| 2 | 2 | 4 |
| 3 | 4 | 4 |
Window Contributions
| Index | Value | Window Size | Contribution |
|---|---|---|---|
| 0 | 0 | 4 | ans[3] = 0 |
| 1 | 1 | 3 | ans[2] = 1 |
| 2 | 2 | 2 | ans[1] = 2 |
| 3 | 4 | 1 | ans[0] = 4 |
Final answer:
[4,2,1,0]
Example 2
nums = [10,20,50,10]
Previous Smaller Array
| Index | Value | Previous Smaller Index |
|---|---|---|
| 0 | 10 | -1 |
| 1 | 20 | 0 |
| 2 | 50 | 1 |
| 3 | 10 | -1 |
Next Smaller Array
| Index | Value | Next Smaller Index |
|---|---|---|
| 0 | 10 | 4 |
| 1 | 20 | 3 |
| 2 | 50 | 3 |
| 3 | 10 | 4 |
Window Contributions
| Index | Value | Window Size | Contribution |
|---|---|---|---|
| 0 | 10 | 4 | ans[3] = 10 |
| 1 | 20 | 2 | ans[1] = 20 |
| 2 | 50 | 1 | ans[0] = 50 |
| 3 | 10 | 4 | ans[3] = 10 |
After propagation:
[50,20,10,10]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each element is pushed and popped from the stack at most once |
| Space | O(n) | Arrays and stacks store up to n elements |
The algorithm is linear because monotonic stacks avoid repeated scanning. Every index participates in stack operations a constant number of times.
The auxiliary arrays left, right, and ans each require linear space.
Test Cases
from typing import List
class Solution:
def findMaximums(self, nums: List[int]) -> List[int]:
n = len(nums)
left = [-1] * n
right = [n] * n
stack = []
for i in range(n):
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
if stack:
left[i] = stack[-1]
stack.append(i)
stack.clear()
for i in range(n - 1, -1, -1):
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
if stack:
right[i] = stack[-1]
stack.append(i)
ans = [0] * n
for i in range(n):
size = right[i] - left[i] - 1
ans[size - 1] = max(ans[size - 1], nums[i])
for i in range(n - 2, -1, -1):
ans[i] = max(ans[i], ans[i + 1])
return ans
sol = Solution()
assert sol.findMaximums([0,1,2,4]) == [4,2,1,0] # provided example
assert sol.findMaximums([10,20,50,10]) == [50,20,10,10] # provided example
assert sol.findMaximums([5]) == [5] # single element
assert sol.findMaximums([1,2,3,4,5]) == [5,4,3,2,1] # strictly increasing
assert sol.findMaximums([5,4,3,2,1]) == [5,4,3,2,1] # strictly decreasing
assert sol.findMaximums([2,2,2,2]) == [2,2,2,2] # all duplicates
assert sol.findMaximums([1,3,2,4]) == [4,2,2,1] # mixed ordering
assert sol.findMaximums([7,7,1,7,7]) == [7,7,1,1,1] # repeated minimum
assert sol.findMaximums([9,8]) == [9,8] # small decreasing
assert sol.findMaximums([1,1,1,2]) == [2,1,1,1] # duplicate boundaries
Test Summary
| Test | Why |
|---|---|
[0,1,2,4] |
Validates increasing sequence |
[10,20,50,10] |
Validates mixed structure |
[5] |
Smallest possible input |
[1,2,3,4,5] |
Strictly increasing case |
[5,4,3,2,1] |
Strictly decreasing case |
[2,2,2,2] |
Duplicate handling |
[1,3,2,4] |
Internal minimum transitions |
[7,7,1,7,7] |
Global minimum dominates large windows |
[9,8] |
Small edge case |
[1,1,1,2] |
Equal values with distinct maximum |
Edge Cases
One important edge case is an array containing all identical values, such as [2,2,2,2]. Duplicate handling is a common source of bugs in monotonic stack problems. If the stack comparisons use inconsistent inequalities, elements may incorrectly overlap ranges or leave gaps. This implementation consistently uses >= while popping, ensuring duplicates are handled correctly and every window receives the proper answer.
Another important case is a strictly increasing array like [1,2,3,4,5]. In this situation, every element becomes the minimum for all windows extending leftward. The algorithm correctly identifies progressively larger ranges, producing answers [5,4,3,2,1].
A strictly decreasing array such as [5,4,3,2,1] is also significant because every new element immediately becomes the smallest seen so far. This stresses the stack popping logic heavily. Since every index is still pushed and popped only once, the algorithm maintains linear complexity.
The smallest valid input, a single-element array like [5], must also work correctly. The element becomes the minimum for the only possible window of size 1, so the answer is simply [5]. The boundary initialization with -1 and n ensures this case works naturally without special handling.