1856. Maximum Subarray Min-Product
Problem Description
Given an array of integers nums
, you need to find the maximum min-product among all possible non-empty subarrays.
The min-product of an array is defined as the minimum value in the array multiplied by the sum of all elements in the array.
For example, if we have the array [3,2,5]
:
- The minimum value is
2
- The sum of all elements is
3 + 2 + 5 = 10
- The min-product is
2 * 10 = 20
Your task is to:
- Consider all possible contiguous subarrays of the given array
- Calculate the min-product for each subarray
- Return the maximum min-product found
Since the answer can be very large, return the result modulo 10^9 + 7
. Note that you should find the maximum min-product first, then apply the modulo operation.
A subarray is a contiguous sequence of elements from the array. For instance, if nums = [1,2,3,4]
, then [2,3]
is a subarray, but [1,3]
is not (elements are not contiguous).
The key insight is that for each element in the array, we can consider it as the minimum element of some subarray. We need to find the optimal subarray boundaries where this element remains the minimum, which would maximize the sum and thus the min-product for that particular minimum value.
Intuition
The key observation is that we want to maximize the product of (minimum value) × (sum of subarray). Since we're dealing with a product, we need to think about how to balance these two factors.
For each element nums[i]
in the array, let's consider what happens if we fix it as the minimum value of a subarray. Once we fix the minimum, we want to maximize the sum of the subarray while ensuring nums[i]
remains the minimum. This means we should extend the subarray as far as possible to the left and right, including all elements that are greater than or equal to nums[i]
.
Think of it this way: if nums[i]
is going to be our minimum value, we should be greedy and include as many elements as possible in our subarray (to maximize the sum), but we must stop when we encounter an element smaller than nums[i]
(otherwise nums[i]
wouldn't be the minimum anymore).
For example, if we have [3, 1, 5, 6, 4, 2]
and we're considering 4
as the minimum:
- We can extend left to include
6
and5
(both ≥ 4) - We stop at
1
on the left (1 < 4) - We can't extend right because
2
< 4 - So the subarray would be
[5, 6, 4]
with min-product =4 × (5+6+4) = 60
This leads us to the problem: for each element, we need to find:
- Left boundary: the rightmost position to the left where an element is strictly less than
nums[i]
- Right boundary: the leftmost position to the right where an element is less than or equal to
nums[i]
The reason for the asymmetry (strictly less on the left, less than or equal on the right) is to avoid counting the same subarray twice when we have duplicate values.
Finding these boundaries efficiently for all elements suggests using a monotonic stack - a classic technique for finding the next/previous smaller element. We can use the stack to maintain elements in increasing order, which helps us quickly identify boundaries.
To calculate the sum efficiently, we use a prefix sum array so that the sum of any subarray can be computed in O(1) time as s[right] - s[left]
.
Learn more about Stack, Prefix Sum and Monotonic Stack patterns.
Solution Approach
The solution uses monotonic stacks to find boundaries and prefix sums for efficient range sum calculation.
Step 1: Find Left Boundaries
We traverse the array from left to right with a monotonic stack to find left[i]
- the rightmost position to the left of i
where nums[left[i]] < nums[i]
.
left = [-1] * n # Initialize with -1 (no element to the left)
stk = []
for i, x in enumerate(nums):
while stk and nums[stk[-1]] >= x:
stk.pop() # Remove elements >= current element
if stk:
left[i] = stk[-1] # Top of [stack](/problems/stack_intro) is the rightmost smaller element
stk.append(i)
The stack maintains indices in increasing order of their values. When we encounter nums[i]
, we pop all elements from the stack that are greater than or equal to nums[i]
. The remaining top element (if any) is the rightmost element smaller than nums[i]
.
Step 2: Find Right Boundaries
Similarly, we traverse from right to left to find right[i]
- the leftmost position to the right of i
where nums[right[i]] <= nums[i]
.
right = [n] * n # Initialize with n (no element to the right)
stk = []
for i in range(n - 1, -1, -1):
while stk and nums[stk[-1]] > nums[i]:
stk.pop() # Remove elements > current element
if stk:
right[i] = stk[-1] # Top of [stack](/problems/stack_intro) is the leftmost smaller or equal element
stk.append(i)
Note the subtle difference: we use >
instead of >=
to handle duplicates correctly and avoid counting the same subarray multiple times.
Step 3: Calculate Prefix Sums
We build a prefix sum array where s[i]
represents the sum of the first i
elements:
s = list(accumulate(nums, initial=0))
This allows us to calculate the sum of subarray [left[i]+1, right[i]-1]
as s[right[i]] - s[left[i] + 1]
in O(1) time.
Step 4: Find Maximum Min-Product
For each element nums[i]
as the minimum value, the subarray extends from left[i] + 1
to right[i] - 1
. The min-product is:
min_product = nums[i] * (s[right[i]] - s[left[i] + 1])
We calculate this for all positions and return the maximum value modulo 10^9 + 7
:
return max((s[right[i]] - s[left[i] + 1]) * x for i, x in enumerate(nums)) % mod
Time Complexity: O(n) - each element is pushed and popped from the stack at most once.
Space Complexity: O(n) - for the left
, right
, s
arrays and the 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 solution with the array nums = [2, 3, 3, 1, 2]
.
Step 1: Find Left Boundaries
We traverse left to right with a monotonic stack to find the rightmost smaller element to the left of each position.
- i=0, x=2: stack=[], no smaller element to left, left[0]=-1, stack=[0]
- i=1, x=3: stack=[0], nums[0]=2 < 3, left[1]=0, stack=[0,1]
- i=2, x=3: stack=[0,1], nums[1]=3 >= 3 so pop 1, nums[0]=2 < 3, left[2]=0, stack=[0,2]
- i=3, x=1: stack=[0,2], nums[2]=3 >= 1 so pop 2, nums[0]=2 >= 1 so pop 0, stack=[], left[3]=-1, stack=[3]
- i=4, x=2: stack=[3], nums[3]=1 < 2, left[4]=3, stack=[3,4]
Result: left = [-1, 0, 0, -1, 3]
Step 2: Find Right Boundaries
We traverse right to left to find the leftmost smaller or equal element to the right.
- i=4, x=2: stack=[], no element to right, right[4]=5, stack=[4]
- i=3, x=1: stack=[4], nums[4]=2 > 1, pop 4, stack=[], right[3]=5, stack=[3]
- i=2, x=3: stack=[3], nums[3]=1 <= 3, right[2]=3, stack=[3,2]
- i=1, x=3: stack=[3,2], nums[2]=3 > 3 is false, right[1]=2, stack=[3,2,1]
- i=0, x=2: stack=[3,2,1], nums[1]=3 > 2, pop 1, nums[2]=3 > 2, pop 2, nums[3]=1 <= 2, right[0]=3, stack=[3,0]
Result: right = [3, 2, 3, 5, 5]
Step 3: Calculate Prefix Sums
s = [0, 2, 5, 8, 9, 11]
where s[i] = sum of first i elements
Step 4: Calculate Min-Products
For each position i, the subarray extends from left[i]+1 to right[i]-1:
- i=0: subarray [0,2], sum = s[3] - s[0] = 8 - 0 = 8, min-product = 2 × 8 = 16
- i=1: subarray [1,1], sum = s[2] - s[1] = 5 - 2 = 3, min-product = 3 × 3 = 9
- i=2: subarray [1,2], sum = s[3] - s[1] = 8 - 2 = 6, min-product = 3 × 6 = 18
- i=3: subarray [0,4], sum = s[5] - s[0] = 11 - 0 = 11, min-product = 1 × 11 = 11
- i=4: subarray [4,4], sum = s[5] - s[4] = 11 - 9 = 2, min-product = 2 × 2 = 4
Maximum min-product = 18 (from position i=2)
The key insight is that for element 3 at position 2, we can extend the subarray to include position 1 (also value 3) but must stop at position 3 (value 1 < 3). This gives us the subarray [3,3] with minimum 3 and sum 6, yielding the maximum min-product of 18.
Solution Implementation
1class Solution:
2 def maxSumMinProduct(self, nums: List[int]) -> int:
3 # Get the length of the input array
4 n = len(nums)
5
6 # Arrays to store the left and right boundaries for each element
7 # left_boundary[i]: rightmost index j where j < i and nums[j] < nums[i]
8 # right_boundary[i]: leftmost index j where j > i and nums[j] < nums[i]
9 left_boundary = [-1] * n
10 right_boundary = [n] * n
11
12 # Find left boundaries using monotonic increasing stack
13 # For each element, find the nearest smaller element to its left
14 stack = []
15 for i, value in enumerate(nums):
16 # Pop elements from stack that are >= current element
17 while stack and nums[stack[-1]] >= value:
18 stack.pop()
19 # If stack is not empty, top element is the left boundary
20 if stack:
21 left_boundary[i] = stack[-1]
22 # Add current index to stack
23 stack.append(i)
24
25 # Find right boundaries using monotonic increasing stack
26 # For each element, find the nearest smaller element to its right
27 stack = []
28 for i in range(n - 1, -1, -1):
29 # Pop elements from stack that are > current element
30 while stack and nums[stack[-1]] > nums[i]:
31 stack.pop()
32 # If stack is not empty, top element is the right boundary
33 if stack:
34 right_boundary[i] = stack[-1]
35 # Add current index to stack
36 stack.append(i)
37
38 # Build prefix sum array for efficient range sum calculation
39 # prefix_sum[i] = sum of nums[0] to nums[i-1]
40 prefix_sum = list(accumulate(nums, initial=0))
41
42 # Define modulo constant for the result
43 MOD = 10**9 + 7
44
45 # Calculate the maximum sum-min product
46 # For each element as minimum, calculate sum of subarray * minimum value
47 # Subarray range: (left_boundary[i] + 1) to (right_boundary[i] - 1)
48 max_product = max(
49 (prefix_sum[right_boundary[i]] - prefix_sum[left_boundary[i] + 1]) * value
50 for i, value in enumerate(nums)
51 )
52
53 # Return the result modulo MOD
54 return max_product % MOD
55
1class Solution {
2 public int maxSumMinProduct(int[] nums) {
3 int n = nums.length;
4
5 // Arrays to store the nearest smaller element indices
6 // leftBoundary[i] = index of nearest smaller element to the left of i (-1 if none)
7 int[] leftBoundary = new int[n];
8 // rightBoundary[i] = index of nearest smaller element to the right of i (n if none)
9 int[] rightBoundary = new int[n];
10
11 // Initialize boundaries
12 Arrays.fill(leftBoundary, -1);
13 Arrays.fill(rightBoundary, n);
14
15 // Monotonic stack to find nearest smaller elements
16 Deque<Integer> stack = new ArrayDeque<>();
17
18 // Find nearest smaller element to the left for each position
19 for (int i = 0; i < n; i++) {
20 // Pop elements from stack that are greater than or equal to current element
21 while (!stack.isEmpty() && nums[stack.peek()] >= nums[i]) {
22 stack.pop();
23 }
24 // If stack has elements, top is the nearest smaller element to the left
25 if (!stack.isEmpty()) {
26 leftBoundary[i] = stack.peek();
27 }
28 // Add current index to stack
29 stack.push(i);
30 }
31
32 // Clear stack for reuse
33 stack.clear();
34
35 // Find nearest smaller element to the right for each position
36 for (int i = n - 1; i >= 0; i--) {
37 // Pop elements from stack that are greater than current element
38 // Note: Using > instead of >= to handle duplicates correctly
39 while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
40 stack.pop();
41 }
42 // If stack has elements, top is the nearest smaller element to the right
43 if (!stack.isEmpty()) {
44 rightBoundary[i] = stack.peek();
45 }
46 // Add current index to stack
47 stack.push(i);
48 }
49
50 // Build prefix sum array for quick range sum calculation
51 // prefixSum[i] = sum of elements from index 0 to i-1
52 long[] prefixSum = new long[n + 1];
53 for (int i = 0; i < n; i++) {
54 prefixSum[i + 1] = prefixSum[i] + nums[i];
55 }
56
57 // Calculate maximum sum-min product
58 long maxProduct = 0;
59 for (int i = 0; i < n; i++) {
60 // For each element as minimum, calculate the subarray sum where it's the minimum
61 // The subarray spans from (leftBoundary[i] + 1) to (rightBoundary[i] - 1)
62 long subarraySum = prefixSum[rightBoundary[i]] - prefixSum[leftBoundary[i] + 1];
63 long product = (long)nums[i] * subarraySum;
64 maxProduct = Math.max(maxProduct, product);
65 }
66
67 // Return result modulo 10^9 + 7
68 final int MOD = (int)1e9 + 7;
69 return (int)(maxProduct % MOD);
70 }
71}
72
1class Solution {
2public:
3 int maxSumMinProduct(vector<int>& nums) {
4 int n = nums.size();
5
6 // left[i] stores the index of the nearest element to the left of i
7 // that is strictly smaller than nums[i]
8 vector<int> left(n, -1);
9
10 // right[i] stores the index of the nearest element to the right of i
11 // that is smaller than or equal to nums[i]
12 vector<int> right(n, n);
13
14 // Monotonic stack to find previous smaller element
15 stack<int> stk;
16
17 // Find the previous smaller element for each index
18 for (int i = 0; i < n; ++i) {
19 // Pop elements that are greater than or equal to current element
20 while (!stk.empty() && nums[stk.top()] >= nums[i]) {
21 stk.pop();
22 }
23 // If stack is not empty, top element is the previous smaller element
24 if (!stk.empty()) {
25 left[i] = stk.top();
26 }
27 stk.push(i);
28 }
29
30 // Clear the stack for reuse
31 stk = stack<int>();
32
33 // Find the next smaller or equal element for each index
34 for (int i = n - 1; i >= 0; --i) {
35 // Pop elements that are strictly greater than current element
36 while (!stk.empty() && nums[stk.top()] > nums[i]) {
37 stk.pop();
38 }
39 // If stack is not empty, top element is the next smaller or equal element
40 if (!stk.empty()) {
41 right[i] = stk.top();
42 }
43 stk.push(i);
44 }
45
46 // Build prefix sum array for range sum queries
47 vector<long long> prefixSum(n + 1, 0);
48 for (int i = 0; i < n; ++i) {
49 prefixSum[i + 1] = prefixSum[i] + nums[i];
50 }
51
52 // Calculate the maximum min-product
53 long long maxProduct = 0;
54 for (int i = 0; i < n; ++i) {
55 // For each element as minimum, calculate the sum of subarray
56 // where nums[i] is the minimum element
57 // The subarray ranges from (left[i] + 1) to (right[i] - 1)
58 long long subarraySum = prefixSum[right[i]] - prefixSum[left[i] + 1];
59 long long minProduct = static_cast<long long>(nums[i]) * subarraySum;
60 maxProduct = max(maxProduct, minProduct);
61 }
62
63 // Apply modulo as per problem requirement
64 const int MOD = 1e9 + 7;
65 return maxProduct % MOD;
66 }
67};
68
1/**
2 * Finds the maximum score of any subarray where score = sum(subarray) * min(subarray)
3 * Uses monotonic stack to find boundaries for each element as minimum
4 * @param nums - Input array of positive integers
5 * @returns Maximum score modulo 10^9 + 7
6 */
7function maxSumMinProduct(nums: number[]): number {
8 const n = nums.length;
9
10 // leftBoundary[i] stores the index of the nearest element to the left that is smaller than nums[i]
11 // -1 means no such element exists (nums[i] is the minimum from start to i)
12 const leftBoundary: number[] = Array(n).fill(-1);
13
14 // rightBoundary[i] stores the index of the nearest element to the right that is smaller or equal to nums[i]
15 // n means no such element exists (nums[i] is the minimum from i to end)
16 const rightBoundary: number[] = Array(n).fill(n);
17
18 // Monotonic increasing stack to find left boundaries
19 const stack: number[] = [];
20
21 // Find left boundary for each element
22 // Stack maintains indices in increasing order of their values
23 for (let i = 0; i < n; ++i) {
24 // Pop elements from stack that are greater than or equal to current element
25 while (stack.length && nums[stack.at(-1)!] >= nums[i]) {
26 stack.pop();
27 }
28 // If stack is not empty, top element is the left boundary
29 if (stack.length) {
30 leftBoundary[i] = stack.at(-1)!;
31 }
32 stack.push(i);
33 }
34
35 // Clear stack for reuse
36 stack.length = 0;
37
38 // Find right boundary for each element (traverse from right to left)
39 for (let i = n - 1; i >= 0; --i) {
40 // Pop elements from stack that are greater than current element
41 while (stack.length && nums[stack.at(-1)!] > nums[i]) {
42 stack.pop();
43 }
44 // If stack is not empty, top element is the right boundary
45 if (stack.length) {
46 rightBoundary[i] = stack.at(-1)!;
47 }
48 stack.push(i);
49 }
50
51 // Build prefix sum array for quick range sum calculation
52 // prefixSum[i] = sum of nums[0] to nums[i-1]
53 const prefixSum: number[] = Array(n + 1).fill(0);
54 for (let i = 0; i < n; ++i) {
55 prefixSum[i + 1] = prefixSum[i] + nums[i];
56 }
57
58 // Calculate maximum product
59 let maxProduct: bigint = 0n;
60 const MOD = 10 ** 9 + 7;
61
62 // For each element as the minimum value in a subarray
63 for (let i = 0; i < n; ++i) {
64 // Calculate sum of subarray where nums[i] is the minimum
65 // Subarray spans from (leftBoundary[i] + 1) to (rightBoundary[i] - 1)
66 const subarraySum = prefixSum[rightBoundary[i]] - prefixSum[leftBoundary[i] + 1];
67
68 // Calculate product: minimum value * sum of subarray
69 const currentProduct = BigInt(nums[i]) * BigInt(subarraySum);
70
71 // Update maximum product
72 if (maxProduct < currentProduct) {
73 maxProduct = currentProduct;
74 }
75 }
76
77 // Return result modulo MOD
78 return Number(maxProduct % BigInt(MOD));
79}
80
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of several linear passes through the array:
- First monotonic stack traversal to find left boundaries: Each element is pushed and popped from the stack at most once, resulting in
O(n)
operations - Second monotonic stack traversal to find right boundaries: Similarly, each element is pushed and popped at most once, giving
O(n)
operations - Building the prefix sum array using
accumulate
:O(n)
time - Final iteration to calculate the maximum sum-min product:
O(n)
time to iterate through all elements
Since all operations are linear and performed sequentially, the overall time complexity is O(n) + O(n) + O(n) + O(n) = O(n)
.
Space Complexity: O(n)
The algorithm uses the following auxiliary data structures:
left
array of sizen
:O(n)
spaceright
array of sizen
:O(n)
spacestk
for monotonic stack operations: At mostn
elements, soO(n)
spaces
prefix sum array of sizen+1
:O(n)
space
The total space complexity is O(n) + O(n) + O(n) + O(n) = O(n)
, where n
is the length of the input array nums
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Duplicate Elements
One of the most common mistakes is using the same comparison operator for both left and right boundary calculations. This can lead to counting the same subarray multiple times when duplicate elements exist.
Incorrect approach:
# Both using >= operator while stack and nums[stack[-1]] >= value: # Left boundary while stack and nums[stack[-1]] >= nums[i]: # Right boundary
Why it's wrong: If we have duplicates like [2, 2, 2]
, using >=
for both boundaries means each 2
could claim the entire array as its domain, causing the same subarray to be counted three times.
Correct approach:
# Left boundary: use >= while stack and nums[stack[-1]] >= value: stack.pop() # Right boundary: use > (strictly greater) while stack and nums[stack[-1]] > nums[i]: stack.pop()
This ensures that when duplicates exist, only the leftmost occurrence can extend to include all duplicates, preventing double counting.
2. Applying Modulo Too Early
Incorrect approach:
# Applying modulo during calculation
max_product = 0
for i, value in enumerate(nums):
product = ((prefix_sum[right_boundary[i]] - prefix_sum[left_boundary[i] + 1]) * value) % MOD
max_product = max(max_product, product)
return max_product
Why it's wrong: The problem asks to find the maximum first, then apply modulo. Applying modulo during calculation can change the comparison results since (a % MOD) > (b % MOD)
doesn't necessarily mean a > b
.
Correct approach:
# Find maximum first, then apply modulo
max_product = max(
(prefix_sum[right_boundary[i]] - prefix_sum[left_boundary[i] + 1]) * value
for i, value in enumerate(nums)
)
return max_product % MOD
3. Off-by-One Errors in Boundary Indices
Common mistake: Confusion about whether boundaries are inclusive or exclusive.
Key points to remember:
left_boundary[i]
is the index of an element strictly smaller thannums[i]
right_boundary[i]
is the index of an element smaller than or equal tonums[i]
- The valid subarray where
nums[i]
is minimum spans fromleft_boundary[i] + 1
toright_boundary[i] - 1
(inclusive) - For prefix sum calculation:
sum[l, r] = prefix_sum[r + 1] - prefix_sum[l]
4. Integer Overflow in Other Languages
While Python handles large integers automatically, in languages like Java or C++, the multiplication can overflow:
Solution for other languages:
// Java example - use long type long product = ((long)rangeSum * nums[i]) % MOD;
5. Incorrect Initialization of Boundaries
Incorrect:
left_boundary = [0] * n # Wrong! This suggests element 0 is always a left boundary right_boundary = [n-1] * n # Wrong! This suggests element n-1 is always a right boundary
Correct:
left_boundary = [-1] * n # -1 indicates no smaller element to the left right_boundary = [n] * n # n indicates no smaller element to the right
These sentinel values correctly handle edge cases where an element is the smallest in its extending range.
How many ways can you arrange the three letters A, B and C?
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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
Want a Structured Path to Master System Design Too? Don’t Miss This!