1856. Maximum Subarray Min Product - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Prefix Sums - Essential for computing range sums in O(1) time after O(n) preprocessing
- Monotonic Stack - Used to efficiently find the nearest smaller element on both sides for each index
- Divide and Conquer - Alternative approach that recursively processes subarrays based on minimum elements
- Segment Tree (optional) - Advanced data structure for O(log n) range minimum queries
1. Brute Force
Intuition
The min-product of a subarray is defined as the minimum element multiplied by the sum of all elements. For each possible subarray, we need to track both the running minimum and the running sum. By trying all subarrays starting from each index, we can compute every possible min-product and find the maximum.
Algorithm
- For each starting index
ifrom0ton-1:- Initialize
total_sum = 0andmini = infinity. - Extend the subarray rightward from
iton-1. - Update
miniwith each new element. - Add each element to
total_sum. - Calculate
mini * total_sumand update the result.
- Initialize
- Return the maximum min-product modulo
10^9 + 7.
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
res, MOD = 0, 1000000007
for i in range(len(nums)):
total_sum = 0
mini = float("inf")
for j in range(i, len(nums)):
mini = min(mini, nums[j])
total_sum += nums[j]
cur = (mini * total_sum) % MOD
res = max(res, cur)
return resTime & Space Complexity
- Time complexity:
- Space complexity: extra space.
2. Divide And Conquer (Brute Force)
Intuition
For any subarray, the minimum element defines the "bottleneck." If we know the minimum element's position, the optimal subarray with that element as minimum spans as far as possible in both directions. This leads to a divide and conquer approach: find the minimum in the current range, calculate the score for the entire range using that minimum, then recursively solve for the left and right portions (excluding the minimum).
Algorithm
- Define a recursive function for range
[l, r]:- Base case: if
l > r, return0. - Find the index of the minimum element in
[l, r]and compute the total sum. - Calculate the current score as
sum * min_element. - Recursively solve for
[l, min_idx - 1]and[min_idx + 1, r]. - Return the maximum of the three values.
- Base case: if
- Call the recursive function on
[0, n-1]and return the result modulo10^9 + 7.
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
MOD = 10**9 + 7
def rec(l, r):
if l > r:
return 0
min_idx = l
total_sum = 0
for i in range(l, r + 1):
total_sum += nums[i]
if nums[i] < nums[min_idx]:
min_idx = i
cur = total_sum * nums[min_idx]
left = rec(l, min_idx - 1)
right = rec(min_idx + 1, r)
return max(cur, left, right)
return rec(0, len(nums) - 1) % MODTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
3. Divide And Conquer (Segment Tree)
Intuition
The brute force divide and conquer spends O(n) time finding the minimum in each range, leading to O(n^2) worst case. We can optimize this using a segment tree that answers range minimum queries in O(log n). Combined with a prefix sum array for O(1) range sum queries, this reduces the overall complexity significantly.
Algorithm
- Build a segment tree that stores the index of the minimum element for each range.
- Build a prefix sum array for O(1) range sum queries.
- Define a recursive function for range
[l, r]:- Base case: if
l > r, return0. - Query the segment tree to find the minimum element's index in
[l, r]. - Compute the range sum using prefix sums.
- Calculate the current score and recursively process left and right portions.
- Base case: if
- Return the maximum result modulo
10^9 + 7.
class SegmentTree:
def __init__(self, N, A):
self.n = N
while (self.n & (self.n - 1)) != 0:
self.n += 1
self.build(N, A)
def build(self, N, A):
self.tree = [(-1, float('inf'))] * (2 * self.n)
for i in range(N):
self.tree[self.n + i] = (i, A[i])
for i in range(self.n - 1, 0, -1):
self.tree[i] = min(self.tree[i << 1], self.tree[i << 1 | 1], key=lambda x: x[1])
def query(self, l, r):
res = (-1, float('inf'))
l += self.n
r += self.n + 1
while l < r:
if l & 1:
res = min(res, self.tree[l], key=lambda x: x[1])
l += 1
if r & 1:
r -= 1
res = min(res, self.tree[r], key=lambda x: x[1])
l >>= 1
r >>= 1
return res[0]
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
MOD = 10**9 + 7
segTree = SegmentTree(len(nums), nums)
prefix_sum = [0] * (len(nums) + 1)
for i in range(0, len(nums)):
prefix_sum[i + 1] += prefix_sum[i] + nums[i]
def rec(l, r):
if l > r:
return 0
min_idx = segTree.query(l, r)
total_sum = prefix_sum[r + 1] - prefix_sum[l]
cur = total_sum * nums[min_idx]
left = rec(l, min_idx - 1)
right = rec(min_idx + 1, r)
return max(cur, left, right)
return rec(0, len(nums) - 1) % MODTime & Space Complexity
- Time complexity:
- Space complexity:
4. Monotonic Stack
Intuition
For each element, we want to find the maximum subarray where that element is the minimum. This means finding the nearest smaller elements on both sides. A monotonic stack efficiently computes these boundaries in linear time. With prefix sums for fast range sums, we can evaluate each element's contribution and find the global maximum.
Algorithm
- Build a prefix sum array.
- Use a monotonic stack (increasing values) to find, for each index:
prev_min[i]: index of the nearest smaller element to the left (or-1).nxt_min[i]: index of the nearest smaller element to the right (orn).
- For each index
i:- The valid range is
[prev_min[i] + 1, nxt_min[i] - 1]. - Compute the range sum using prefix sums.
- Calculate
nums[i] * range_sumand update the result.
- The valid range is
- Return the maximum modulo
10^9 + 7.
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
n = len(nums)
prefix_sum = [0] * (n + 1)
for i in range(n):
prefix_sum[i + 1] = prefix_sum[i] + nums[i]
prev_min, nxt_min = [-1] * n, [n] * n
stack = []
for i in range(n):
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
if stack:
prev_min[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:
nxt_min[i] = stack[-1]
stack.append(i)
res = 0
for i in range(n):
l, r = prev_min[i] + 1, nxt_min[i] - 1
total_sum = prefix_sum[r + 1] - prefix_sum[l]
res = max(res, nums[i] * total_sum)
return res % (10 ** 9 + 7)Time & Space Complexity
- Time complexity:
- Space complexity:
5. Monotonic Stack (Space Optimized) - I
Intuition
We can avoid storing separate boundary arrays by processing elements as they are popped from the stack. When an element is popped (because a smaller element is found), we immediately know its right boundary. The left boundary is the element currently at the top of the stack. We also track the starting index of each element's potential range as we push to the stack.
Algorithm
- Build a prefix sum array.
- Iterate through the array, maintaining a stack of (start_index, value) pairs:
- When a smaller element is encountered, pop elements from the stack.
- For each popped element, calculate its contribution using the range from its start index to the current index.
- Update the new element's start index to the earliest popped element's start.
- After iteration, process remaining stack elements (their range extends to the end).
- Return the maximum modulo
10^9 + 7.
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
n = len(nums)
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + nums[i]
res = 0
stack = []
for i, num in enumerate(nums):
new_start = i
while stack and stack[-1][1] > num:
start, val = stack.pop()
total = prefix[i] - prefix[start]
res = max(res, val * total)
new_start = start
stack.append((new_start, num))
while stack:
start, val = stack.pop()
total = prefix[n] - prefix[start]
res = max(res, val * total)
return res % (10**9 + 7)Time & Space Complexity
- Time complexity:
- Space complexity:
6. Monotonic Stack (Space Optimized) - II
Intuition
This variation uses an index-only stack and processes all elements in a single pass by treating the position after the array as a sentinel. When we pop an element, the current position is its right boundary, and the new stack top (or -1) is its left boundary. This eliminates the need to store value pairs in the stack.
Algorithm
- Build a prefix sum array.
- Iterate from index
0ton(inclusive, wherenacts as sentinel):- While the stack is non-empty and current element is smaller (or we reached the sentinel):
- Pop index
jfrom the stack. - Left boundary is stack top + 1 (or
0if empty). - Right boundary is current index - 1.
- Calculate
nums[j] * range_sumand update the result.
- Pop index
- Push current index to stack.
- While the stack is non-empty and current element is smaller (or we reached the sentinel):
- Return the maximum modulo
10^9 + 7.
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
n = len(nums)
prefix_sum = [0] * (n + 1)
for i in range(n):
prefix_sum[i + 1] = prefix_sum[i] + nums[i]
res, stack = 0, []
for i in range(n + 1):
while stack and (i == n or nums[i] < nums[stack[-1]]):
j = stack.pop()
start = 0 if not stack else stack[-1] + 1
res = max(res, nums[j] * (prefix_sum[i] - prefix_sum[start]))
stack.append(i)
return res % (10 ** 9 + 7)Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Integer Overflow Before Taking Modulo
The min-product can be extremely large (minimum value up to 10^7 times sum up to 10^14), exceeding 32-bit integer limits. You must use 64-bit integers for intermediate calculations and only apply modulo at the very end when returning the result. Applying modulo during comparisons breaks the max logic since you're comparing reduced values.
Confusing Min-Product with Other Formulas
The min-product is min(subarray) * sum(subarray), which combines both the minimum element and the subarray sum. This is different from problems involving just the minimum times length, or just the sum. Make sure to compute prefix sums for efficient range sum queries and track minimums correctly.
Off-by-One Errors with Prefix Sum Boundaries
When using prefix sums, the sum of elements from index l to r (inclusive) is prefix[r+1] - prefix[l], not prefix[r] - prefix[l]. Getting this boundary wrong by one index leads to incorrect subarray sums and wrong answers.