LeetCode 2104 - Sum of Subarray Ranges
The problem gives us an integer array nums, and asks us to compute the sum of the ranges of every possible non-empty contiguous subarray. The range of a subarray is defined as: For every possible subarray, we calculate its range, then add all of those ranges together.
Difficulty: 🟡 Medium
Topics: Array, Stack, Monotonic Stack
Solution
LeetCode 2104 - Sum of Subarray Ranges
Problem Understanding
The problem gives us an integer array nums, and asks us to compute the sum of the ranges of every possible non-empty contiguous subarray.
The range of a subarray is defined as:
maximum element - minimum element
For every possible subarray, we calculate its range, then add all of those ranges together.
For example, if:
nums = [1,2,3]
The subarrays are:
[1] -> range = 0
[2] -> range = 0
[3] -> range = 0
[1,2] -> range = 1
[2,3] -> range = 1
[1,2,3] -> range = 2
The final answer is:
0 + 0 + 0 + 1 + 1 + 2 = 4
The input array can contain both positive and negative integers. The array length is at most 1000, which means a quadratic solution is acceptable for the original constraints, but the follow-up explicitly asks for an O(n) solution.
The key challenge is that the number of subarrays is O(n²). If we compute the minimum and maximum separately for every subarray, the total complexity can become O(n³), which is inefficient.
Several edge cases are important:
- Arrays with all equal values, because every subarray range becomes
0 - Arrays containing negative numbers
- Arrays with duplicate values, because monotonic stack logic must carefully avoid double counting
- Very small arrays such as length
1 - Strictly increasing or strictly decreasing arrays, because they create extreme stack behavior
The problem guarantees:
numsis non-empty- Values fit within signed 32-bit integers
- The final answer may exceed 32-bit integer range, so larger integer types are necessary
Approaches
Brute Force Approach
The most straightforward solution is to enumerate every subarray.
For each starting index, we expand the subarray one element at a time while maintaining the current minimum and maximum values. The range for the current subarray is:
current_max - current_min
We add this value to the answer.
A naive brute force solution would recompute the minimum and maximum for every subarray independently, leading to O(n³) complexity. However, we can optimize slightly by updating the running minimum and maximum incrementally while expanding the subarray. That reduces the complexity to O(n²).
This works because every subarray is examined exactly once, and its range is computed correctly.
Although O(n²) passes for the given constraints, it does not satisfy the follow-up requirement of linear time.
Optimal Approach Using Monotonic Stacks
The key insight is that:
sum of subarray ranges
=
(sum of all subarray maximums)
-
(sum of all subarray minimums)
Instead of evaluating every subarray individually, we calculate how much each element contributes as:
- a maximum
- a minimum
For a particular element nums[i], we determine:
- how many subarrays use it as the maximum
- how many subarrays use it as the minimum
If an element appears as the maximum in k subarrays, then its total contribution to the maximum sum is:
nums[i] * k
Similarly for minimums.
To count these efficiently, we use monotonic stacks to find:
- Previous Less Element
- Next Less Element
- Previous Greater Element
- Next Greater Element
These boundaries determine how many subarrays can extend left and right while keeping the current element as the minimum or maximum.
This transforms the problem into a linear-time contribution counting problem.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | Expands every subarray while tracking min and max |
| Optimal | O(n) | O(n) | Uses monotonic stacks to count contributions |
Algorithm Walkthrough
Step 1: Convert the Problem Into Contribution Counting
Instead of directly computing every subarray range, rewrite the formula as:
sum(maximums) - sum(minimums)
This allows us to process maximum contributions and minimum contributions independently.
Step 2: Count Minimum Contributions
For each index i, determine how many subarrays have nums[i] as their minimum value.
We need two boundaries:
- The nearest smaller element on the left
- The nearest smaller or equal element on the right
These boundaries ensure duplicates are handled correctly without double counting.
If:
left = distance to previous smaller
right = distance to next smaller-or-equal
then the number of subarrays where nums[i] is the minimum is:
left * right
Its contribution becomes:
nums[i] * left * right
We use a monotonic increasing stack to compute these boundaries efficiently.
Step 3: Count Maximum Contributions
Repeat the same process for maximums.
Now we use:
- previous greater element
- next greater or equal element
We maintain a monotonic decreasing stack.
Again:
count = left * right
Contribution:
nums[i] * left * right
Step 4: Compute Final Answer
The final answer is:
total_maximum_contribution - total_minimum_contribution
Why it works
Every subarray has exactly one maximum and one minimum. Instead of enumerating subarrays directly, we count how many subarrays each element dominates as the maximum or minimum.
The monotonic stack guarantees that for every element, we correctly identify the maximal interval where it remains the minimum or maximum. Multiplying the number of valid left choices by the number of valid right choices gives the exact number of subarrays contributed by that element.
Because every subarray contributes exactly once to the maximum sum and exactly once to the minimum sum, subtracting the totals produces the correct answer.
Python Solution
from typing import List
class Solution:
def subArrayRanges(self, nums: List[int]) -> int:
n = len(nums)
def sum_subarray_mins() -> int:
stack = []
result = 0
for i in range(n + 1):
current = nums[i] if i < n else float('-inf')
while stack and nums[stack[-1]] > current:
mid = stack.pop()
left = mid - (stack[-1] if stack else -1)
right = i - mid
result += nums[mid] * left * right
stack.append(i)
return result
def sum_subarray_maxs() -> int:
stack = []
result = 0
for i in range(n + 1):
current = nums[i] if i < n else float('inf')
while stack and nums[stack[-1]] < current:
mid = stack.pop()
left = mid - (stack[-1] if stack else -1)
right = i - mid
result += nums[mid] * left * right
stack.append(i)
return result
return sum_subarray_maxs() - sum_subarray_mins()
The implementation separates the computation into two helper functions:
sum_subarray_minssum_subarray_maxs
Each helper computes the total contribution of all elements when acting as either the minimum or maximum.
The stack stores indices rather than values because we need distances between positions. The stack maintains monotonic order:
- Increasing order for minimums
- Decreasing order for maximums
A sentinel iteration at n forces all remaining elements out of the stack so their contributions are processed.
When an index is popped:
left = mid - previous_index
right = next_index - mid
The number of subarrays where this element dominates is:
left * right
The contribution formula:
nums[mid] * left * right
adds the element's total effect across all valid subarrays.
Finally, subtracting the minimum contribution sum from the maximum contribution sum produces the answer.
Go Solution
func subArrayRanges(nums []int) int64 {
n := len(nums)
sumSubarrayMins := func() int64 {
stack := []int{}
var result int64 = 0
for i := 0; i <= n; i++ {
current := -(1 << 60)
if i < n {
current = nums[i]
}
for len(stack) > 0 && nums[stack[len(stack)-1]] > current {
mid := stack[len(stack)-1]
stack = stack[:len(stack)-1]
leftBoundary := -1
if len(stack) > 0 {
leftBoundary = stack[len(stack)-1]
}
left := mid - leftBoundary
right := i - mid
result += int64(nums[mid]) * int64(left) * int64(right)
}
stack = append(stack, i)
}
return result
}
sumSubarrayMaxs := func() int64 {
stack := []int{}
var result int64 = 0
for i := 0; i <= n; i++ {
current := 1 << 60
if i < n {
current = nums[i]
}
for len(stack) > 0 && nums[stack[len(stack)-1]] < current {
mid := stack[len(stack)-1]
stack = stack[:len(stack)-1]
leftBoundary := -1
if len(stack) > 0 {
leftBoundary = stack[len(stack)-1]
}
left := mid - leftBoundary
right := i - mid
result += int64(nums[mid]) * int64(left) * int64(right)
}
stack = append(stack, i)
}
return result
}
return sumSubarrayMaxs() - sumSubarrayMins()
}
The Go implementation follows the same algorithmic structure as the Python version.
One important difference is integer handling. The result can exceed 32-bit integer range, so the function returns int64, and all contribution calculations are explicitly converted to int64.
Go does not provide built-in infinity values for integers, so large sentinel values are manually chosen:
-(1 << 60)
1 << 60
Slices are used as stacks with append and truncation operations.
Worked Examples
Example 1
nums = [1,2,3]
Maximum Contributions
| Index | Value | Previous Greater | Next Greater | Left Choices | Right Choices | Contribution |
|---|---|---|---|---|---|---|
| 0 | 1 | -1 | 1 | 1 | 1 | 1 |
| 1 | 2 | -1 | 2 | 2 | 1 | 4 |
| 2 | 3 | -1 | 3 | 3 | 1 | 9 |
Total maximum sum:
1 + 4 + 9 = 14
Minimum Contributions
| Index | Value | Previous Smaller | Next Smaller | Left Choices | Right Choices | Contribution |
|---|---|---|---|---|---|---|
| 0 | 1 | -1 | 3 | 1 | 3 | 3 |
| 1 | 2 | 0 | 3 | 1 | 2 | 4 |
| 2 | 3 | 1 | 3 | 1 | 1 | 3 |
Total minimum sum:
3 + 4 + 3 = 10
Final answer:
14 - 10 = 4
Example 2
nums = [1,3,3]
Maximum Contributions
| Index | Value | Contribution |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 3 | 12 |
| 2 | 3 | 5 |
Total maximum sum:
18
Minimum Contributions
| Index | Value | Contribution |
|---|---|---|
| 0 | 1 | 3 |
| 1 | 3 | 6 |
| 2 | 3 | 5 |
Total minimum sum:
14
Final answer:
18 - 14 = 4
Example 3
nums = [4,-2,-3,4,1]
The algorithm computes:
sum(maximums) = 59 + sum(minimums)
After processing all stack contributions:
final answer = 59
The important aspect of this example is handling:
- Negative numbers
- Duplicate maximums
- Mixed increasing and decreasing patterns
The monotonic stack correctly processes all of them in linear time.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Every index is pushed and popped at most once in each stack |
| Space | O(n) | The monotonic stack may store all indices |
The critical observation is that each element enters and leaves the stack only once. Although there are nested loops, the total number of stack operations across the entire algorithm is linear.
This is a standard amortized analysis for monotonic stack problems.
Test Cases
from typing import List
class Solution:
def subArrayRanges(self, nums: List[int]) -> int:
n = len(nums)
def sum_subarray_mins():
stack = []
result = 0
for i in range(n + 1):
current = nums[i] if i < n else float('-inf')
while stack and nums[stack[-1]] > current:
mid = stack.pop()
left = mid - (stack[-1] if stack else -1)
right = i - mid
result += nums[mid] * left * right
stack.append(i)
return result
def sum_subarray_maxs():
stack = []
result = 0
for i in range(n + 1):
current = nums[i] if i < n else float('inf')
while stack and nums[stack[-1]] < current:
mid = stack.pop()
left = mid - (stack[-1] if stack else -1)
right = i - mid
result += nums[mid] * left * right
stack.append(i)
return result
return sum_subarray_maxs() - sum_subarray_mins()
sol = Solution()
assert sol.subArrayRanges([1,2,3]) == 4 # basic increasing example
assert sol.subArrayRanges([1,3,3]) == 4 # duplicates
assert sol.subArrayRanges([4,-2,-3,4,1]) == 59 # mixed positive and negative
assert sol.subArrayRanges([1]) == 0 # single element
assert sol.subArrayRanges([5,5,5]) == 0 # all equal values
assert sol.subArrayRanges([1,2]) == 1 # simple increasing pair
assert sol.subArrayRanges([2,1]) == 1 # simple decreasing pair
assert sol.subArrayRanges([-1,-2,-3]) == 4 # all negative numbers
assert sol.subArrayRanges([1,2,3,4]) == 10 # strictly increasing
assert sol.subArrayRanges([4,3,2,1]) == 10 # strictly decreasing
assert sol.subArrayRanges([10,-10,10]) == 60 # large swings in range
Test Summary
| Test | Why |
|---|---|
[1,2,3] |
Basic increasing example |
[1,3,3] |
Validates duplicate handling |
[4,-2,-3,4,1] |
Mixed positive and negative values |
[1] |
Smallest possible input |
[5,5,5] |
All ranges should be zero |
[1,2] |
Small increasing pair |
[2,1] |
Small decreasing pair |
[-1,-2,-3] |
Negative number handling |
[1,2,3,4] |
Strictly increasing sequence |
[4,3,2,1] |
Strictly decreasing sequence |
[10,-10,10] |
Large range differences |
Edge Cases
Single Element Array
If the array contains only one element, the only subarray is the element itself. Since the minimum and maximum are equal, the range is always zero.
This can easily cause off-by-one mistakes in stack boundary calculations. The implementation handles it correctly because:
left = 1
right = 1
and the maximum and minimum contributions cancel out.
Arrays With Duplicate Values
Duplicate values are one of the most error-prone parts of monotonic stack problems.
If both sides use strict comparisons, some subarrays may be counted multiple times. If both use non-strict comparisons, some subarrays may never be counted.
The implementation carefully mixes strict and non-strict comparisons so each subarray is assigned to exactly one representative element.
This guarantees correctness without double counting.
Strictly Increasing or Decreasing Arrays
Monotonic arrays produce the deepest possible stack behavior.
For example:
[1,2,3,4,5]
creates a continuously increasing stack for minimum calculations.
A poorly designed algorithm might degrade into quadratic behavior. However, in this implementation, each index is still pushed and popped only once, so the total complexity remains linear.
Negative Numbers
Since ranges depend on differences between maximum and minimum values, negative numbers must be handled carefully.
For example:
[-3,-2,-1]
still has positive ranges because:
max - min
remains positive.
The algorithm works correctly because it never relies on positivity, only on relative ordering between elements.