1856. Maximum Subarray Min Product - Explanation

Problem Link



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

  1. For each starting index i from 0 to n-1:
    • Initialize total_sum = 0 and mini = infinity.
    • Extend the subarray rightward from i to n-1.
    • Update mini with each new element.
    • Add each element to total_sum.
    • Calculate mini * total_sum and update the result.
  2. 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 res

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(1)O(1) 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

  1. Define a recursive function for range [l, r]:
    • Base case: if l > r, return 0.
    • 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.
  2. Call the recursive function on [0, n-1] and return the result modulo 10^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) % MOD

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n) 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

  1. Build a segment tree that stores the index of the minimum element for each range.
  2. Build a prefix sum array for O(1) range sum queries.
  3. Define a recursive function for range [l, r]:
    • Base case: if l > r, return 0.
    • 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.
  4. 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) % MOD

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

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

  1. Build a prefix sum array.
  2. 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 (or n).
  3. 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_sum and update the result.
  4. 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: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Build a prefix sum array.
  2. 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.
  3. After iteration, process remaining stack elements (their range extends to the end).
  4. 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: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Build a prefix sum array.
  2. Iterate from index 0 to n (inclusive, where n acts as sentinel):
    • While the stack is non-empty and current element is smaller (or we reached the sentinel):
      • Pop index j from the stack.
      • Left boundary is stack top + 1 (or 0 if empty).
      • Right boundary is current index - 1.
      • Calculate nums[j] * range_sum and update the result.
    • Push current index to stack.
  3. 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: O(n)O(n)
  • Space complexity: O(n)O(n)

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.