Skip to content
← All posts

Sum of Subarray Minimums: Monotonic Stack Contribution Technique

6 min read
leetcodeproblemmediumarraysstacksdynamic-programming

LeetCode 907 Sum of Subarray Minimums is a beautiful example of the "contribution technique." Instead of examining every subarray and finding its minimum, you flip the question: for each element, how many subarrays have it as the minimum? Once you know that count, you can compute the answer in O(n) time using a monotonic stack.

The problem

Given an array of integers arr, find the sum of min(b) for every contiguous subarray b of arr. Return the answer modulo 10^9 + 7.

Input:  arr = [3, 1, 2, 4]
Output: 17

There are 10 subarrays total. You take the minimum of each one and sum them up.

3i=01i=12i=24i=3arr = [3, 1, 2, 4]subarraymin[3]3[1]1[2]2[4]4[3, 1]1[1, 2]1[2, 4]2[3, 1, 2]1[1, 2, 4]1[3, 1, 2, 4]1sum = 3+1+2+4+1+1+2+1+1+1 = 17
All 10 subarrays of [3, 1, 2, 4] with their minimums. The answer is the sum of all minimums: 17.

The brute force is to enumerate all O(n^2) subarrays, find each minimum in O(n), and sum them. That is O(n^3), or O(n^2) with some optimization. But the array can have up to 30,000 elements, so we need O(n).

The key insight: contribution counting

Instead of asking "what is the minimum of subarray [i..j]?", ask the reverse: "for element arr[i], how many subarrays have arr[i] as their minimum?"

If you can answer that for every index, then the total sum is simply:

sum of arr[i] * count[i] for all i

The trick is computing count[i] efficiently. This is where the monotonic stack comes in.

The contribution technique appears in many problems. Whenever you are summing a property over all subarrays or subsequences, consider flipping the question to ask how much each individual element contributes.

Finding left and right boundaries

For arr[i] to be the minimum of a subarray, that subarray cannot extend past a smaller element. So you need two pieces of information:

  1. left[i]: how far left can a subarray extend while arr[i] remains the minimum? This is the distance from i to the previous element that is strictly smaller.
  2. right[i]: how far right can a subarray extend? This is the distance from i to the next element that is smaller or equal.

The number of subarrays where arr[i] is the minimum equals left[i] * right[i]. The left boundary gives you the number of valid starting points, and the right boundary gives the number of valid ending points.

Why "strictly smaller" on the left but "smaller or equal" on the right? To avoid double-counting. If two elements have the same value, you need a consistent tiebreaker. Using strict on one side and non-strict on the other ensures each subarray is counted exactly once.

The algorithm

Two passes with a monotonic stack:

Left pass (left to right): For each index i, pop all elements from the stack that are greater than or equal to arr[i]. The remaining top gives the previous strictly smaller element. If the stack is empty, the boundary extends all the way to the start.

Right pass (right to left): For each index i, pop all elements from the stack that are strictly greater than arr[i]. The remaining top gives the next smaller-or-equal element. If the stack is empty, the boundary extends to the end.

After both passes, compute the answer as the sum of arr[i] * left[i] * right[i] for all i.

The code

def sumSubarrayMins(arr):
    MOD = 10**9 + 7
    n = len(arr)
    left = [0] * n
    right = [0] * n
    stack = []
    for i in range(n):
        while stack and arr[stack[-1]] >= arr[i]:
            stack.pop()
        left[i] = i - stack[-1] if stack else i + 1
        stack.append(i)
    stack = []
    for i in range(n - 1, -1, -1):
        while stack and arr[stack[-1]] > arr[i]:
            stack.pop()
        right[i] = stack[-1] - i if stack else n - i
        stack.append(i)
    result = 0
    for i in range(n):
        result = (result + arr[i] * left[i] * right[i]) % MOD
    return result

The first loop computes left boundaries. The condition arr[stack[-1]] >= arr[i] pops elements that are greater or equal, so the remaining top (if any) is strictly smaller. The second loop computes right boundaries with arr[stack[-1]] > arr[i], popping only strictly greater elements. This asymmetry handles duplicates correctly.

Step-by-step walkthrough

Let's trace through arr = [3, 1, 2, 4]. We perform two passes: a left pass scanning left to right and a right pass scanning right to left. The L values show how many positions to the left each element dominates. The R values show positions to the right.

Left pass, i=0: Stack empty. left[0] = i+1 = 1. Push index 0.

3i=01i=12i=24i=3L=1stack3i=0

No previous smaller element. Element 3 can be the minimum starting from the left boundary (1 position).

Left pass, i=1: arr[0]=3 >= arr[1]=1, pop 0. Stack empty, left[1] = 2. Push 1.

3i=01i=12i=24i=3L=1L=2stack1i=1

Value 1 is smaller than 3, so 3 is popped. With the stack empty, left[1] = i+1 = 2. Element 1 dominates 2 positions to its left.

Left pass, i=2: arr[1]=1 < arr[2]=2, stop. left[2] = 2-1 = 1. Push 2.

3i=01i=12i=24i=3L=1L=2L=1stack1i=12i=2

Stack top has index 1 with value 1, which is smaller than 2. So left[2] = i - stack[-1] = 2 - 1 = 1.

Left pass, i=3: arr[2]=2 < arr[3]=4, stop. left[3] = 3-2 = 1. Push 3.

3i=01i=12i=24i=3L=1L=2L=1L=1stack1i=12i=24i=3

Stack top has index 2 with value 2, which is smaller than 4. So left[3] = 3 - 2 = 1. Left pass complete.

Right pass, i=3: Stack empty. right[3] = n-i = 1. Push 3.

3i=01i=12i=24i=3L=1L=2L=1L=1R=1stack4i=3

Scanning right to left now. No next smaller element for index 3. right[3] = n - i = 4 - 3 = 1.

Right pass, i=2: arr[3]=4 > arr[2]=2, pop 3. Stack empty, right[2] = 2. Push 2.

3i=01i=12i=24i=3L=1L=2L=1R=2L=1R=1stack2i=2

Value 4 at index 3 is not smaller than 2, so pop it. Stack is empty, so right[2] = n - i = 4 - 2 = 2.

Right pass, i=1: arr[2]=2 > arr[1]=1, pop 2. Stack empty, right[1] = 3. Push 1.

3i=01i=12i=24i=3L=1L=2R=3L=1R=2L=1R=1stack1i=1

Value 2 at index 2 is not smaller than 1, so pop. Stack empty, right[1] = 4 - 1 = 3. Element 1 dominates 3 positions to its right.

Right pass, i=0: arr[1]=1 is not > arr[0]=3. right[0] = 1-0 = 1. Push 0.

3i=01i=12i=24i=3L=1R=1L=2R=3L=1R=2L=1R=1stack3i=01i=1

Stack top is index 1 with value 1, which is not strictly greater than 3. So right[0] = stack[-1] - i = 1 - 0 = 1. Right pass complete.

Final contributions: arr[i] * left[i] * right[i] for each i.

3i=01i=12i=24i=3L=1R=1L=2R=3L=1R=2L=1R=1stackempty

3*1*1 + 1*2*3 + 2*1*2 + 4*1*1 = 3 + 6 + 4 + 4 = 17. Answer: 17.

Notice the final step: once you have left and right arrays, the contribution of each element is just a multiplication. Element 1 at index 1 has left=2 and right=3, meaning it is the minimum of 23=6 subarrays. Its contribution is 16=6, which accounts for a big chunk of the answer.

Complexity analysis

ApproachTimeSpace
Brute force (all subarrays)O(n^2)O(1)
Monotonic stack (contribution)O(n)O(n)

Time: O(n). Each index is pushed onto the stack once and popped at most once in each pass. Two passes of O(n) each give O(n) total.

Space: O(n). The stack, left array, and right array each use O(n) space.

The building blocks

This problem combines two reusable patterns:

Monotonic stack for boundaries

The core loop for finding the previous smaller element:

stack = []
for i in range(n):
    while stack and arr[stack[-1]] >= arr[i]:
        stack.pop()
    left[i] = i - stack[-1] if stack else i + 1
    stack.append(i)

This is the same pattern used in Daily Temperatures, Largest Rectangle in Histogram, and Online Stock Span. The only differences are the comparison direction and what you compute when popping.

Contribution counting

Once you have the boundaries, the final accumulation is:

result = sum(arr[i] * left[i] * right[i] for i in range(n)) % MOD

This pattern of "count how many substructures each element participates in" appears in many sum-over-subarrays problems.

If you can write the monotonic stack boundary loop from memory, you can solve Sum of Subarray Minimums, Largest Rectangle in Histogram, and several other medium/hard problems quickly. The contribution formula is then just one line of accumulation.

Edge cases

Before submitting, verify your solution handles:

  • All equal elements like [5, 5, 5, 5]: each element is the minimum of some subarrays. The asymmetric comparison (>= left, > right) prevents double-counting.
  • Strictly increasing like [1, 2, 3, 4]: element at index 0 is the minimum of all subarrays that include it (4 subarrays). Each subsequent element has a smaller left boundary.
  • Strictly decreasing like [4, 3, 2, 1]: element at index 3 is the minimum of all 4 subarrays that include it.
  • Single element [7]: answer is 7. left[0]=1, right[0]=1, contribution = 7.
  • Large values with modulo: the problem specifies mod 10^9 + 7. Apply the modulo at each accumulation step to prevent overflow.

From understanding to recall

You now understand how the contribution technique works with a monotonic stack. The algorithm has three parts: left pass, right pass, accumulation. Each part is short, but the details matter. The asymmetric comparison (>= versus >) is easy to mix up under pressure. The direction of the right pass (iterating backwards) is another detail that trips people up.

Spaced repetition helps lock in these details. Instead of re-reading this explanation in three weeks when you see a similar problem, you practice writing the two-pass boundary code from scratch at increasing intervals. After a few repetitions, the asymmetric comparison becomes second nature and you can focus on recognizing when contribution counting applies.

Related posts

CodeBricks breaks the monotonic stack and contribution technique into their reusable pieces and drills them individually. You type the boundary loops from scratch each time, building the muscle memory so that when you see a sum-over-subarrays problem in an interview, the code flows without hesitation.