LeetCode 907 - Sum of Subarray Minimums

The problem asks us to compute the sum of the minimum value of every possible contiguous subarray of a given array. For an array arr, every contiguous slice of the array is considered a subarray. For each subarray, we determine its minimum element.

LeetCode Problem 907

Difficulty: 🟡 Medium
Topics: Array, Dynamic Programming, Stack, Monotonic Stack

Solution

Problem Understanding

The problem asks us to compute the sum of the minimum value of every possible contiguous subarray of a given array.

For an array arr, every contiguous slice of the array is considered a subarray. For each subarray, we determine its minimum element. The final answer is the sum of all of those minimum values.

For example, if the array is:

[3,1,2]

The subarrays are:

[3]       minimum = 3
[1]       minimum = 1
[2]       minimum = 2
[3,1]     minimum = 1
[1,2]     minimum = 1
[3,1,2]   minimum = 1

The answer becomes:

3 + 1 + 2 + 1 + 1 + 1 = 9

The constraints are important:

  • The array length can be as large as 3 * 10^4
  • Each value can also be as large as 3 * 10^4

A naive solution that generates every subarray and scans for its minimum would be far too slow. Since the number of subarrays is approximately n^2, and finding the minimum naively takes another O(n) time, the total complexity could reach O(n^3).

Even an optimized brute force solution that maintains running minimums would still require O(n^2) time, which is too slow for 30,000 elements.

The problem guarantees:

  • The array is non-empty
  • All values are positive integers
  • The result may become very large, so we must return it modulo 10^9 + 7

Important edge cases include:

  • Arrays with a single element
  • Strictly increasing arrays
  • Strictly decreasing arrays
  • Arrays containing duplicate values
  • Large arrays where overflow becomes a concern

Duplicate values are especially important because they affect how we count subarrays uniquely without double counting contributions.

Approaches

Brute Force Approach

The straightforward solution is to enumerate every possible subarray.

For each starting index i, we expand the subarray to every ending index j >= i. While expanding, we maintain the minimum value seen so far.

For example:

arr = [3,1,2]

Start at index 0:
[3]       min = 3
[3,1]     min = 1
[3,1,2]   min = 1

Start at index 1:
[1]       min = 1
[1,2]     min = 1

Start at index 2:
[2]       min = 2

This avoids rescanning the entire subarray each time because we update the minimum incrementally.

The algorithm is correct because every subarray is visited exactly once, and its minimum is added to the answer.

However, the time complexity is still O(n^2), which is too slow for arrays of size 30,000.

Optimal Approach, Monotonic Stack

The key insight is that instead of asking:

"What is the minimum for every subarray?"

we can reverse the perspective and ask:

"For how many subarrays is each element the minimum?"

Suppose an element arr[i] is the minimum for some collection of subarrays. If we can count exactly how many such subarrays exist, then its total contribution becomes:

arr[i] * number_of_subarrays_where_it_is_minimum

To compute this efficiently, we determine:

  • How many consecutive elements to the left are greater than the current element
  • How many consecutive elements to the right are greater than or equal to the current element

These boundaries define all subarrays where arr[i] is the minimum.

A monotonic increasing stack allows us to find these boundaries in linear time.

The subtle handling of duplicates is crucial:

  • On one side we use strictly greater (>)
  • On the other side we use greater than or equal (>=)

This prevents duplicate minimum values from being counted multiple times.

Approach Time Complexity Space Complexity Notes
Brute Force O(n²) O(1) Enumerates every subarray and tracks running minimum
Optimal O(n) O(n) Uses monotonic stack to count each element's contribution

Algorithm Walkthrough

Step 1: Compute Previous Smaller Element

For each index i, we want to know the nearest index to the left containing a strictly smaller value.

We maintain a monotonic increasing stack containing indices.

While the top of the stack has a value greater than the current value, we pop it.

After popping:

  • If the stack is empty, there is no smaller element to the left
  • Otherwise, the top of the stack is the previous smaller element

This determines how far left the current element can extend while remaining the minimum.

Step 2: Compute Next Smaller or Equal Element

Next, we compute the nearest index to the right containing a value smaller than or equal to the current value.

Again, we use a monotonic increasing stack.

This time, while the top of the stack has a value greater than or equal to the current value, we pop it.

Using >= here is critical for handling duplicates correctly.

This determines how far right the current element can extend while remaining the minimum.

Step 3: Count Valid Subarrays

For each index i:

left_count = i - previous_smaller_index
right_count = next_smaller_or_equal_index - i

The number of subarrays where arr[i] is the minimum becomes:

left_count * right_count

This works because:

  • We can independently choose any valid left boundary
  • We can independently choose any valid right boundary

The Cartesian product gives all valid subarrays.

Step 4: Add Contribution

Each element contributes:

arr[i] * left_count * right_count

We sum all contributions and take modulo 10^9 + 7.

Why it works

Every subarray has exactly one minimum element assigned to it by the boundary rules. The monotonic stack efficiently determines the maximal range where each element remains the minimum.

Using strict comparison on one side and non-strict comparison on the other guarantees that duplicate values are assigned consistently without overlap or double counting.

Python Solution

from typing import List

class Solution:
    def sumSubarrayMins(self, arr: List[int]) -> int:
        MOD = 10**9 + 7
        n = len(arr)

        previous_smaller = [-1] * n
        next_smaller_equal = [n] * n

        stack = []

        # Previous smaller element
        for i in range(n):
            while stack and arr[stack[-1]] > arr[i]:
                stack.pop()

            if stack:
                previous_smaller[i] = stack[-1]

            stack.append(i)

        stack = []

        # Next smaller or equal element
        for i in range(n - 1, -1, -1):
            while stack and arr[stack[-1]] >= arr[i]:
                stack.pop()

            if stack:
                next_smaller_equal[i] = stack[-1]

            stack.append(i)

        result = 0

        for i in range(n):
            left_count = i - previous_smaller[i]
            right_count = next_smaller_equal[i] - i

            contribution = arr[i] * left_count * right_count
            result = (result + contribution) % MOD

        return result

The implementation begins by initializing arrays that will store the nearest smaller boundaries for every element.

The first loop computes the previous smaller element for each index. The stack is maintained in increasing order of values. Whenever a larger element appears on top of the stack, it can no longer serve as a valid previous smaller boundary, so it is removed.

The second loop computes the next smaller or equal element by iterating from right to left. Using >= ensures duplicate values are handled consistently.

After both boundary arrays are built, the algorithm calculates how many subarrays each element contributes to as the minimum. The contribution formula directly follows from the number of valid left and right expansions.

Finally, the answer is accumulated modulo 10^9 + 7.

Go Solution

func sumSubarrayMins(arr []int) int {
    const MOD int = 1_000_000_007

    n := len(arr)

    previousSmaller := make([]int, n)
    nextSmallerEqual := make([]int, n)

    for i := 0; i < n; i++ {
        previousSmaller[i] = -1
        nextSmallerEqual[i] = n
    }

    stack := []int{}

    // Previous smaller element
    for i := 0; i < n; i++ {
        for len(stack) > 0 && arr[stack[len(stack)-1]] > arr[i] {
            stack = stack[:len(stack)-1]
        }

        if len(stack) > 0 {
            previousSmaller[i] = stack[len(stack)-1]
        }

        stack = append(stack, i)
    }

    stack = []int{}

    // Next smaller or equal element
    for i := n - 1; i >= 0; i-- {
        for len(stack) > 0 && arr[stack[len(stack)-1]] >= arr[i] {
            stack = stack[:len(stack)-1]
        }

        if len(stack) > 0 {
            nextSmallerEqual[i] = stack[len(stack)-1]
        }

        stack = append(stack, i)
    }

    result := 0

    for i := 0; i < n; i++ {
        leftCount := i - previousSmaller[i]
        rightCount := nextSmallerEqual[i] - i

        contribution := ((arr[i] * leftCount) % MOD * rightCount) % MOD
        result = (result + contribution) % MOD
    }

    return result
}

The Go implementation follows the same algorithmic structure as the Python version.

A slice is used to simulate the stack. Elements are pushed with append and popped by reslicing.

Go requires more explicit handling of integer overflow concerns. Although the problem constraints fit within 64-bit arithmetic comfortably, modular multiplication is still applied carefully during contribution computation.

Unlike Python, Go does not automatically support negative indexing or dynamic integer sizing, so boundary initialization is handled explicitly.

Worked Examples

Example 1

arr = [3,1,2,4]

Previous Smaller Array

Index Value Stack Before Previous Smaller Stack After
0 3 [] -1 [0]
1 1 [0] -1 [1]
2 2 [1] 1 [1,2]
3 4 [1,2] 2 [1,2,3]

Result:

previous_smaller = [-1,-1,1,2]

Next Smaller or Equal Array

Index Value Stack Before Next Smaller Equal Stack After
3 4 [] 4 [3]
2 2 [3] 4 [2]
1 1 [2] 4 [1]
0 3 [1] 1 [1,0]

Result:

next_smaller_equal = [1,4,4,4]

Contribution Calculation

Index Value Left Count Right Count Contribution
0 3 1 1 3
1 1 2 3 6
2 2 1 2 4
3 4 1 1 4

Final answer:

3 + 6 + 4 + 4 = 17

Example 2

arr = [11,81,94,43,3]

Previous Smaller Array

Index Value Previous Smaller
0 11 -1
1 81 0
2 94 1
3 43 0
4 3 -1

Next Smaller or Equal Array

Index Value Next Smaller Equal
0 11 4
1 81 3
2 94 3
3 43 4
4 3 5

Contribution Calculation

Index Value Left Count Right Count Contribution
0 11 1 4 44
1 81 1 2 162
2 94 1 1 94
3 43 3 1 129
4 3 5 1 15

Final answer:

44 + 162 + 94 + 129 + 15 = 444

Complexity Analysis

Measure Complexity Explanation
Time O(n) Each index is pushed and popped from the stack at most once
Space O(n) Stack and boundary arrays both require linear space

Although there are multiple passes through the array, each pass is linear. The crucial observation is that each element enters and leaves the monotonic stack at most one time, making the total stack work proportional to n.

Test Cases

from typing import List

class Solution:
    def sumSubarrayMins(self, arr: List[int]) -> int:
        MOD = 10**9 + 7
        n = len(arr)

        previous_smaller = [-1] * n
        next_smaller_equal = [n] * n

        stack = []

        for i in range(n):
            while stack and arr[stack[-1]] > arr[i]:
                stack.pop()

            if stack:
                previous_smaller[i] = stack[-1]

            stack.append(i)

        stack = []

        for i in range(n - 1, -1, -1):
            while stack and arr[stack[-1]] >= arr[i]:
                stack.pop()

            if stack:
                next_smaller_equal[i] = stack[-1]

            stack.append(i)

        result = 0

        for i in range(n):
            left_count = i - previous_smaller[i]
            right_count = next_smaller_equal[i] - i

            result += arr[i] * left_count * right_count

        return result % (10**9 + 7)

solution = Solution()

assert solution.sumSubarrayMins([3,1,2,4]) == 17  # provided example
assert solution.sumSubarrayMins([11,81,94,43,3]) == 444  # provided example
assert solution.sumSubarrayMins([5]) == 5  # single element
assert solution.sumSubarrayMins([1,2,3,4]) == 20  # strictly increasing
assert solution.sumSubarrayMins([4,3,2,1]) == 20  # strictly decreasing
assert solution.sumSubarrayMins([2,2,2]) == 12  # duplicate values
assert solution.sumSubarrayMins([1,1,1,1]) == 10  # all equal
assert solution.sumSubarrayMins([10,9,8,7,6]) == 110  # decreasing larger values
assert solution.sumSubarrayMins([1,3,2]) == 10  # mixed ordering
assert solution.sumSubarrayMins([71,55,82,55]) == 593  # duplicate minima handling
Test Why
[3,1,2,4] Validates the basic example
[11,81,94,43,3] Tests mixed increases and decreases
[5] Smallest valid input
[1,2,3,4] Increasing sequence behavior
[4,3,2,1] Decreasing sequence behavior
[2,2,2] Duplicate handling correctness
[1,1,1,1] All equal values
[10,9,8,7,6] Larger decreasing pattern
[1,3,2] Local minimum inside array
[71,55,82,55] Tests tie-breaking logic carefully

Edge Cases

A single-element array is the simplest edge case. Since there is only one subarray, the answer must equal that element itself. Implementations that incorrectly initialize boundaries or mishandle empty stacks can fail here. This solution handles it naturally because the left and right spans both become 1.

Arrays containing duplicate values are a common source of bugs. If both boundary searches use the same comparison operator, some subarrays may be counted multiple times or missed entirely. This implementation solves that problem by using > for the previous smaller search and >= for the next smaller search, creating a consistent ownership rule for duplicates.

Strictly increasing arrays stress whether the algorithm correctly extends ranges to the left. Every element except the first has a previous smaller element immediately before it. The monotonic stack naturally preserves this structure and computes the spans correctly.

Strictly decreasing arrays create the opposite pattern. Every new element becomes the smallest so far, causing repeated stack pops. This is an important stress case for stack correctness. Even though many pops occur, each element is still pushed and popped only once overall, preserving linear time complexity.

Large arrays can produce enormous intermediate sums. Without modulo arithmetic, integer overflow could occur in some languages. The implementation applies modulo operations during accumulation to ensure correctness and compliance with the problem requirements.