907. Sum of Subarray Minimums
Problem Description
You are given an array of integers arr
. Your task is to find the sum of minimum values across all possible contiguous subarrays of arr
.
For example, if arr = [3, 1, 2, 4]
, the contiguous subarrays are:
[3]
with minimum value 3[3, 1]
with minimum value 1[3, 1, 2]
with minimum value 1[3, 1, 2, 4]
with minimum value 1[1]
with minimum value 1[1, 2]
with minimum value 1[1, 2, 4]
with minimum value 1[2]
with minimum value 2[2, 4]
with minimum value 2[4]
with minimum value 4
The sum of all these minimum values is 3 + 1 + 1 + 1 + 1 + 1 + 1 + 2 + 2 + 4 = 17
.
Since the answer can be very large, return the result modulo 10^9 + 7
.
The solution uses a monotonic stack approach to efficiently calculate how many subarrays have each element as their minimum value. For each element arr[i]
, it finds:
left[i]
: the index of the nearest element to the left that is smaller thanarr[i]
(or -1 if none exists)right[i]
: the index of the nearest element to the right that is smaller than or equal toarr[i]
(or n if none exists)
The number of subarrays where arr[i]
is the minimum is (i - left[i]) × (right[i] - i)
. This represents all possible subarrays that can be formed by choosing a starting point between left[i] + 1
and i
, and an ending point between i
and right[i] - 1
.
The asymmetry in the comparison (using >=
for left boundary but >
for right boundary) prevents counting duplicate subarrays when there are equal elements in the array.
Intuition
Instead of finding the minimum for every possible subarray (which would be inefficient), we can flip the problem around: for each element, count how many subarrays have that element as their minimum value, then multiply by the element's value.
Think about it this way - if an element arr[i]
is the minimum in 10 different subarrays, it contributes arr[i] × 10
to our final sum. So the key question becomes: how many subarrays have arr[i]
as their minimum?
For arr[i]
to be the minimum of a subarray, that subarray cannot extend beyond any smaller elements on either side. This means:
- The subarray cannot include any element to the left that is smaller than
arr[i]
- The subarray cannot include any element to the right that is smaller than
arr[i]
So we need to find the "boundaries" for each element - how far left and right can we extend while arr[i]
remains the minimum?
If we find:
- The nearest smaller element to the left at position
left[i]
- The nearest smaller element to the right at position
right[i]
Then any subarray containing arr[i]
as its minimum must:
- Start somewhere between
left[i] + 1
andi
(we havei - left[i]
choices) - End somewhere between
i
andright[i] - 1
(we haveright[i] - i
choices)
Total subarrays with arr[i]
as minimum = (i - left[i]) × (right[i] - i)
A monotonic stack is perfect for finding "nearest smaller element" problems. We maintain a stack of indices where the corresponding values are in increasing order. When we encounter a new element:
- Pop elements from the stack that are greater than or equal to the current element
- The top of the stack (if any remains) gives us the nearest smaller element
- Push the current element's index onto the stack
We do this twice - once from left to right to find left[i]
, and once from right to left to find right[i]
. The slight asymmetry (using >=
for left but >
for right) handles duplicate values correctly, ensuring each subarray is counted exactly once.
Learn more about Stack, Dynamic Programming and Monotonic Stack patterns.
Solution Approach
The implementation uses a monotonic stack to find the boundaries for each element efficiently in O(n)
time.
Step 1: Initialize Arrays
- Create
left
array initialized with-1
(representing no smaller element to the left) - Create
right
array initialized withn
(representing no smaller element to the right) - These arrays will store the boundaries for each element
Step 2: Find Left Boundaries We traverse the array from left to right with a monotonic stack:
stk = []
for i, v in enumerate(arr):
while stk and arr[stk[-1]] >= v:
stk.pop()
if stk:
left[i] = stk[-1]
stk.append(i)
- The stack maintains indices of elements in increasing order of their values
- When we see element
v
at indexi
:- Pop all elements from stack that are
>= v
(they can't be the left boundary) - The remaining top element (if any) is the nearest smaller element to the left
- Push current index
i
to maintain the monotonic property
- Pop all elements from stack that are
Step 3: Find Right Boundaries We traverse the array from right to left with a fresh monotonic stack:
stk = []
for i in range(n - 1, -1, -1):
while stk and arr[stk[-1]] > arr[i]:
stk.pop()
if stk:
right[i] = stk[-1]
stk.append(i)
- Similar process but moving right to left
- Note the condition is
>
(not>=
) to handle duplicates correctly - This ensures that when multiple elements have the same value, only one of them "claims" each subarray
Step 4: Calculate the Sum
sum((i - left[i]) * (right[i] - i) * v for i, v in enumerate(arr)) % mod
For each element at index i
with value v
:
(i - left[i])
= number of possible starting positions for subarrays(right[i] - i)
= number of possible ending positions for subarrays- Total contribution =
(i - left[i]) × (right[i] - i) × v
Why the Asymmetry in Comparisons?
Using >=
for left boundaries and >
for right boundaries prevents double-counting when duplicate values exist. Consider array [2, 3, 2]
:
- For the first
2
at index 0: left boundary is -1, right boundary is 2 - For the second
2
at index 2: left boundary is -1, right boundary is 3 - This ensures subarray
[2, 3, 2]
is counted only once (for the first2
)
Time Complexity: O(n)
- each element is pushed and popped from the stack at most once
Space Complexity: O(n)
- for the left
, right
arrays and stack
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with a small example: arr = [3, 1, 2, 4]
Step 1: Find Left Boundaries (nearest smaller element to the left)
We traverse left to right with a monotonic stack:
- i=0, v=3: Stack is empty →
left[0] = -1
, push 0. Stack: [0] - i=1, v=1: Pop 0 (since arr[0]=3 >= 1) → Stack empty →
left[1] = -1
, push 1. Stack: [1] - i=2, v=2: arr[1]=1 < 2, keep it →
left[2] = 1
, push 2. Stack: [1, 2] - i=3, v=4: arr[2]=2 < 4, keep it →
left[3] = 2
, push 3. Stack: [1, 2, 3]
Result: left = [-1, -1, 1, 2]
Step 2: Find Right Boundaries (nearest smaller or equal element to the right)
We traverse right to left with a fresh stack:
- i=3, v=4: Stack is empty →
right[3] = 4
, push 3. Stack: [3] - i=2, v=2: Pop 3 (since arr[3]=4 > 2) → Stack empty →
right[2] = 4
, push 2. Stack: [2] - i=1, v=1: Pop 2 (since arr[2]=2 > 1) → Stack empty →
right[1] = 4
, push 1. Stack: [1] - i=0, v=3: arr[1]=1 not > 3, keep it →
right[0] = 1
, push 0. Stack: [1, 0]
Result: right = [1, 4, 4, 4]
Step 3: Calculate Contributions
For each element, calculate how many subarrays have it as minimum:
-
arr[0] = 3:
- Can start from indices: 0 to 0 (since left[0]=-1) → 1 choice
- Can end at indices: 0 to 0 (since right[0]=1) → 1 choice
- Subarrays: [3] → Count = 1 × 1 = 1
- Contribution: 3 × 1 = 3
-
arr[1] = 1:
- Can start from indices: 0 to 1 (since left[1]=-1) → 2 choices
- Can end at indices: 1 to 3 (since right[1]=4) → 3 choices
- Subarrays: [3,1], [3,1,2], [3,1,2,4], [1], [1,2], [1,2,4] → Count = 2 × 3 = 6
- Contribution: 1 × 6 = 6
-
arr[2] = 2:
- Can start from indices: 2 to 2 (since left[2]=1) → 1 choice
- Can end at indices: 2 to 3 (since right[2]=4) → 2 choices
- Subarrays: [2], [2,4] → Count = 1 × 2 = 2
- Contribution: 2 × 2 = 4
-
arr[3] = 4:
- Can start from indices: 3 to 3 (since left[3]=2) → 1 choice
- Can end at indices: 3 to 3 (since right[3]=4) → 1 choice
- Subarrays: [4] → Count = 1 × 1 = 1
- Contribution: 4 × 1 = 4
Total Sum: 3 + 6 + 4 + 4 = 17
This matches our expected result! The monotonic stack approach efficiently found the boundaries for each element, allowing us to calculate each element's contribution to the final sum in O(n) time.
Solution Implementation
1class Solution:
2 def sumSubarrayMins(self, arr: List[int]) -> int:
3 """
4 Calculate the sum of minimum values in all subarrays.
5
6 For each element, find how many subarrays have it as the minimum.
7 This is done by finding the boundary indices where the element is the minimum.
8
9 Args:
10 arr: List of integers
11
12 Returns:
13 Sum of all subarray minimums modulo 10^9 + 7
14 """
15 n = len(arr)
16
17 # left[i]: index of previous element less than arr[i] (or -1 if none)
18 left_boundaries = [-1] * n
19 # right[i]: index of next element less than or equal to arr[i] (or n if none)
20 right_boundaries = [n] * n
21
22 # Build left boundaries using monotonic stack
23 # Stack maintains indices in increasing order of their values
24 stack = []
25 for i, value in enumerate(arr):
26 # Pop elements greater than or equal to current value
27 while stack and arr[stack[-1]] >= value:
28 stack.pop()
29 # If stack not empty, top element is the previous smaller element
30 if stack:
31 left_boundaries[i] = stack[-1]
32 stack.append(i)
33
34 # Build right boundaries using monotonic stack
35 # Process from right to left
36 stack = []
37 for i in range(n - 1, -1, -1):
38 # Pop elements greater than current value (strict inequality for right side)
39 while stack and arr[stack[-1]] > arr[i]:
40 stack.pop()
41 # If stack not empty, top element is the next smaller or equal element
42 if stack:
43 right_boundaries[i] = stack[-1]
44 stack.append(i)
45
46 # Calculate the sum of minimums
47 # For each element at index i with value v:
48 # - It's the minimum in (i - left_boundaries[i]) subarrays ending at i
49 # - It's the minimum in (right_boundaries[i] - i) subarrays starting at i
50 # - Total subarrays where arr[i] is minimum: product of both counts
51 MOD = 10**9 + 7
52 total_sum = sum(
53 (i - left_boundaries[i]) * (right_boundaries[i] - i) * value
54 for i, value in enumerate(arr)
55 ) % MOD
56
57 return total_sum
58
1class Solution {
2 public int sumSubarrayMins(int[] arr) {
3 int n = arr.length;
4
5 // Arrays to store the index of previous/next smaller element for each position
6 int[] previousSmaller = new int[n];
7 int[] nextSmaller = new int[n];
8
9 // Initialize with boundary values
10 Arrays.fill(previousSmaller, -1); // No smaller element on left means -1
11 Arrays.fill(nextSmaller, n); // No smaller element on right means n
12
13 // Stack to maintain indices in monotonic increasing order
14 Deque<Integer> stack = new ArrayDeque<>();
15
16 // Find previous smaller element for each position
17 for (int i = 0; i < n; i++) {
18 // Pop elements from stack that are greater than or equal to current element
19 while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
20 stack.pop();
21 }
22 // If stack has elements, top is the previous smaller element
23 if (!stack.isEmpty()) {
24 previousSmaller[i] = stack.peek();
25 }
26 // Add current index to stack
27 stack.push(i);
28 }
29
30 // Clear stack for reuse
31 stack.clear();
32
33 // Find next smaller element for each position
34 for (int i = n - 1; i >= 0; i--) {
35 // Pop elements from stack that are strictly greater than current element
36 // Note: Using > instead of >= to handle duplicates correctly
37 while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
38 stack.pop();
39 }
40 // If stack has elements, top is the next smaller element
41 if (!stack.isEmpty()) {
42 nextSmaller[i] = stack.peek();
43 }
44 // Add current index to stack
45 stack.push(i);
46 }
47
48 // Constants for modulo arithmetic
49 final int MOD = (int) 1e9 + 7;
50 long result = 0;
51
52 // Calculate contribution of each element as minimum in subarrays
53 for (int i = 0; i < n; i++) {
54 // Count of subarrays where arr[i] is minimum
55 // Left range: from (previousSmaller[i] + 1) to i
56 // Right range: from i to (nextSmaller[i] - 1)
57 long leftCount = i - previousSmaller[i];
58 long rightCount = nextSmaller[i] - i;
59
60 // Add contribution: (number of subarrays) * (element value)
61 long contribution = (leftCount * rightCount % MOD) * arr[i] % MOD;
62 result = (result + contribution) % MOD;
63 }
64
65 return (int) result;
66 }
67}
68
1class Solution {
2public:
3 int sumSubarrayMins(vector<int>& arr) {
4 int n = arr.size();
5
6 // left[i]: index of the previous smaller element (or -1 if none exists)
7 vector<int> left(n, -1);
8 // right[i]: index of the next smaller or equal element (or n if none exists)
9 vector<int> right(n, n);
10
11 // Monotonic stack to find previous smaller element for each position
12 stack<int> monoStack;
13
14 // Find previous smaller element for each index
15 for (int i = 0; i < n; ++i) {
16 // Pop elements that are greater than or equal to current element
17 while (!monoStack.empty() && arr[monoStack.top()] >= arr[i]) {
18 monoStack.pop();
19 }
20 // If stack is not empty, top element is the previous smaller element
21 if (!monoStack.empty()) {
22 left[i] = monoStack.top();
23 }
24 monoStack.push(i);
25 }
26
27 // Clear the stack for next computation
28 monoStack = stack<int>();
29
30 // Find next smaller or equal element for each index
31 for (int i = n - 1; i >= 0; --i) {
32 // Pop elements that are strictly greater than current element
33 // Note: using > instead of >= to handle duplicates correctly
34 while (!monoStack.empty() && arr[monoStack.top()] > arr[i]) {
35 monoStack.pop();
36 }
37 // If stack is not empty, top element is the next smaller or equal element
38 if (!monoStack.empty()) {
39 right[i] = monoStack.top();
40 }
41 monoStack.push(i);
42 }
43
44 // Calculate the sum of minimums for all subarrays
45 long long totalSum = 0;
46 const int MOD = 1e9 + 7;
47
48 for (int i = 0; i < n; ++i) {
49 // Count of subarrays where arr[i] is the minimum
50 // Left count: subarrays starting from (left[i] + 1) to i
51 // Right count: subarrays ending from i to (right[i] - 1)
52 long long leftCount = i - left[i];
53 long long rightCount = right[i] - i;
54
55 // Contribution of arr[i] as minimum in all valid subarrays
56 long long contribution = (leftCount * rightCount % MOD) * arr[i] % MOD;
57
58 totalSum = (totalSum + contribution) % MOD;
59 }
60
61 return totalSum;
62 }
63};
64
1/**
2 * Calculates the sum of minimums of all subarrays in the given array
3 * @param arr - Input array of numbers
4 * @returns Sum of all subarray minimums modulo 10^9 + 7
5 */
6function sumSubarrayMins(arr: number[]): number {
7 const n: number = arr.length;
8
9 // left[i] stores the index of the previous smaller element for arr[i]
10 // -1 means no smaller element exists to the left
11 const left: number[] = Array(n).fill(-1);
12
13 // right[i] stores the index of the next smaller or equal element for arr[i]
14 // n means no smaller or equal element exists to the right
15 const right: number[] = Array(n).fill(n);
16
17 // Monotonic stack to find previous smaller elements
18 const stack: number[] = [];
19
20 // Find previous smaller element for each position
21 for (let i = 0; i < n; i++) {
22 // Pop elements from stack that are greater than or equal to current element
23 while (stack.length > 0 && arr[stack[stack.length - 1]] >= arr[i]) {
24 stack.pop();
25 }
26
27 // If stack is not empty, top element is the previous smaller element
28 if (stack.length > 0) {
29 left[i] = stack[stack.length - 1];
30 }
31
32 // Push current index to stack
33 stack.push(i);
34 }
35
36 // Clear the stack for reuse
37 stack.length = 0;
38
39 // Find next smaller or equal element for each position (traverse from right to left)
40 for (let i = n - 1; i >= 0; i--) {
41 // Pop elements from stack that are strictly greater than current element
42 while (stack.length > 0 && arr[stack[stack.length - 1]] > arr[i]) {
43 stack.pop();
44 }
45
46 // If stack is not empty, top element is the next smaller or equal element
47 if (stack.length > 0) {
48 right[i] = stack[stack.length - 1];
49 }
50
51 // Push current index to stack
52 stack.push(i);
53 }
54
55 // Modulo value for preventing integer overflow
56 const MOD: number = 1000000007;
57 let result: number = 0;
58
59 // Calculate contribution of each element as minimum in subarrays
60 for (let i = 0; i < n; i++) {
61 // Number of subarrays where arr[i] is the minimum:
62 // (i - left[i]) choices for left boundary * (right[i] - i) choices for right boundary
63 const leftCount: number = i - left[i];
64 const rightCount: number = right[i] - i;
65 const contribution: number = (((leftCount * rightCount) % MOD) * arr[i]) % MOD;
66
67 result = (result + contribution) % MOD;
68 }
69
70 return result;
71}
72
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of three main parts:
- First loop: Iterates through the array once to fill the
left
array using a monotonic stack. Each element is pushed and popped from the stack at most once, resulting inO(n)
operations. - Second loop: Iterates through the array in reverse to fill the
right
array using a monotonic stack. Similarly, each element is pushed and popped at most once, resulting inO(n)
operations. - Final sum calculation: Iterates through the array once to compute the sum, performing
O(1)
operations for each element, totalingO(n)
.
Overall time complexity: O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
The algorithm uses the following additional space:
left
array: storesn
elements, requiringO(n)
spaceright
array: storesn
elements, requiringO(n)
spacestk
(stack): can contain at mostn
elements in the worst case, requiringO(n)
space
Overall space complexity: O(n) + O(n) + O(n) = O(n)
where n
is the length of the array arr
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Duplicates
One of the most common mistakes is using symmetric comparisons for both left and right boundaries. If you use >=
for both directions or >
for both directions, you'll either double-count or miss subarrays when duplicate values exist.
Incorrect Implementation:
# WRONG: Using >= for both boundaries
for i, v in enumerate(arr):
while stk and arr[stk[-1]] >= v: # Left boundary
stk.pop()
# Later...
for i in range(n - 1, -1, -1):
while stk and arr[stk[-1]] >= arr[i]: # Right boundary - WRONG!
stk.pop()
Problem Example: For array [2, 2, 2]
, using >=
for both would cause each element to claim no subarrays (each would have boundaries that exclude themselves), resulting in 0 instead of the correct answer.
Solution: Use >=
for left boundary and >
for right boundary (or vice versa, but be consistent):
# Correct: >= for left, > for right while stk and arr[stk[-1]] >= v: # Left boundary while stk and arr[stk[-1]] > arr[i]: # Right boundary
2. Integer Overflow
The sum can become very large before applying modulo, potentially causing overflow in languages with fixed integer sizes.
Incorrect Implementation:
# WRONG: Applying modulo only at the end
total = 0
for i, v in enumerate(arr):
total += (i - left[i]) * (right[i] - i) * v
return total % MOD # Overflow may have already occurred
Solution: Apply modulo during accumulation or use Python's arbitrary precision integers:
# Better: Apply modulo during accumulation if needed
total = 0
for i, v in enumerate(arr):
contribution = (i - left[i]) * (right[i] - i) * v
total = (total + contribution) % MOD
return total
3. Stack Reuse Without Clearing
Forgetting to clear or reinitialize the stack between finding left and right boundaries.
Incorrect Implementation:
stk = []
# Find left boundaries
for i, v in enumerate(arr):
# ... process left boundaries
# WRONG: Forgot to clear stack!
# stk still contains elements from left boundary calculation
for i in range(n - 1, -1, -1):
# ... process right boundaries with dirty stack
Solution: Always reinitialize the stack:
stk = []
# Find left boundaries
for i, v in enumerate(arr):
# ... process left boundaries
stk = [] # Clear the stack!
# Find right boundaries
for i in range(n - 1, -1, -1):
# ... process right boundaries
4. Incorrect Boundary Initialization
Using wrong default values for boundaries can lead to incorrect subarray counts.
Incorrect Implementation:
# WRONG: Initializing with 0 instead of -1 for left left = [0] * n # Should be -1 right = [n-1] * n # Should be n
Problem: This would incorrectly calculate the number of subarrays. For example, if arr[0]
has no smaller element to its left, using left[0] = 0
would give (0 - 0) = 0
subarrays instead of (0 - (-1)) = 1
.
Solution: Use -1
for left boundaries and n
for right boundaries:
left = [-1] * n # Correct: -1 indicates no smaller element to the left right = [n] * n # Correct: n indicates no smaller element to the right
Which of the following shows the order of node visit in a Breadth-first Search?
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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!