Skip to main content

3430. Subarrays Max Min Sum

Maximum and Minimum Sums of at Most Size K Subarrays

After thinking for a bit, this problem really only tests two things:

  1. Whether we're familiar with basic properties of monotonic stacks
  2. Whether we can integrate a monotonic stack with other data structures

Making Traversal Logic Convenient

The task is to find maximum and minimum of every subarray of length at most kk for input array, and return the summation of all those maximums and minimums.

Most natural traversal approach is to let each visited index ii serve as subarray's right end, then look at all valid subarrays ending at ii to find their respective max and min.

This ensures we capture exactly the right information. Nothing more and nothing less.

Since our problem limits subarray length to at most kk, we must control which starting indices jj are valid for the current right end ii.

My habit for subarray-style problems is to let the visited index ii be subarray's right end, so I can look left to filter valid starting indices jj — whether moving, counting, summing up, or multiplying.

Biggest advantage is the traversal logic becomes genuinely convenient.

Boundary Control

When index ii serves as right end, first thing we do is determine which starting indices jj are valid:

those jj satisfying max(0,ik+1)ji\max(0, i - k + 1) \leq j \leq i.

Once valid range for jj is established, the harder part follows.

Efficiently Finding Subarray Max and Min

The answer we need is S=Σi=0n1(MaxSumi+MinSumi)S = \Sigma_{i = 0}^{n - 1} (MaxSum_i + MinSum_i):

I. MaxSumi=Σj=max(0,ik+1)imax(nums[j:i+1])MaxSum_i = \Sigma_{j = max(0, i - k + 1)}^i \max(nums[j:i + 1])

— sum of maximum values across all valid subarrays ending at ii.

II. MinSumi=Σj=max(0,ik+1)imin(nums[j:i+1])MinSum_i = \Sigma_{j = max(0, i - k + 1)}^i \min(nums[j:i + 1])

— sum of minimum values across all valid subarrays ending at ii.

How do we compute these two sums efficiently?

I. Starting From Default Case

Start with MaxSumiMaxSum_i inheriting the value of MaxSumi1MaxSum_{i - 1}, then make two adjustments:

Always happens: add nums[i]nums[i] for its contribution as a maximum, as there's now a single-element subarray nums[i]nums[i].

Conditional: if ik+1>0i - k + 1 > 0, subtract the max value of subarrays containing nums[ik]nums[i - k].

The subarray nums[ik:i]nums[i - k:i] of length kk was valid when subarray's right end was i1i - 1.

Now that nums[i]nums[i] enters, the leftmost element nums[ik]nums[i - k] must retire to let subarray length stay at kk.

This is why we do boundary control. After these adjustments, scan for any further corrections.

II. Incremental Comparison: Responsibilities Transitions

Every valid subarray ending at ii contains nums[i]nums[i].

So if nums[i]nums[i1]nums[i] \geq nums[i - 1] (1), none of these subarrays can have nums[i1]nums[i - 1] as their maximum.

Why include equality? We traverse left to right, if two adjacent elements are equal, definitely prefer the newer one: it arrived later so it can stay longer 😁

In this case, nums[i1]nums[i - 1] was the maximum for ll times of all valid subarrays ending at i1i - 1.

Those ll times are now fully transferred to nums[i]nums[i], producing a net increase of

(nums[i]nums[i1])×l(nums[i] - nums[i - 1]) \times l added to MaxSumiMaxSum_i.

Since nums[i]nums[i] unseated nums[i1]nums[i - 1], it must also compare against nums[i2],,nums[max(0,ik+1)]nums[i - 2], \ldots, nums[max(0, i - k + 1)].

When do we stop? When we hit the first nums[j]nums[j] that is greater than nums[i]nums[i].

Everything to the left of nums[j]nums[j] is even larger, so nums[i]nums[i] can't beat them.

Isn't this a monotonic decreasing stack approach?

Pop all elements that aren't greater than nums[i]nums[i], collect their transferred shares, and push nums[i]nums[i] into stack.

Since MaxSumiMaxSum_i can be tracked in real time using a monotonic decreasing stack, MinSumiMinSum_i can naturally be handled with a monotonic increasing stack.

Only difference: transfer condition flips. For monotonic increasing stack, nums[i]nums[i1]nums[i] \leq nums[i - 1] triggers handoff (2),

Which is the opposite inequality compared to (1) for MaxSumiMaxSum_i.

III. Monotonic Stack Meets Queue

As mentioned, every iteration enforces boundary control. Have to discard max/min contributions from subarrays that are no longer in current valid range.

Such a removal is FIFO, so we merge queue and monotonic stack into a deque that supports both FIFO and LIFO.

Each element on stack is a tuple of (Index, Number, Shares), each field is a critical role:

I. Index: we need to know where the max/min element is located.

This allows boundary control to determine when it falls out of current window and must retire.

II. Number: numerical value of the max or min is needed, since both stacks are always comparing against newly visited element.

III. Shares: among all valid subarrays ending at the current index, the contribution count of an element serving as the max (in decreasing stack) or min (for increasing stack).

Which is actually the ll from our handoff explanation above. It's what allows MaxSumiMaxSum_i and MinSumiMinSum_i to correctly adjust.

Think of it like bonuses: issued upfront at an estimated amount, then corrected at the end based on actual performance.

Max Min Stacks Efficiency Each element enters both stacks exactly once for each. Every element leaves two stacks at most twice in total. Time and space complexity: O(n)O(n).

from collections import deque


def compute_max_min_sum(nums: list[int], k: int) -> int: # LeetCode Q.3430.
subarrays_max_min_sum = 0

max_stack: deque[list[int]] = deque([]) # Format: [idx, num, shares].
subarrays_max_sum = 0

min_stack: deque[list[int]] = deque([]) # Format: [idx, num, shares].
subarrays_min_sum = 0

for end_idx, num in enumerate(nums):
start_idx = max(0, end_idx - k + 1)

# Window start idx slides by 1: must update stacks' info.
if start_idx > 0:
max_stack[0][2] -= 1 # Decrement stack's front num shares.
subarrays_max_sum -= max_stack[0][1]

if max_stack[0][0] < start_idx: # Front num out of window.
max_stack.popleft()

min_stack[0][2] -= 1 # Decrement stack's front num shares.
subarrays_min_sum -= min_stack[0][1]

if min_stack[0][0] < start_idx: # Front num out of window.
min_stack.popleft()

max_shares = 1 # Base case.
subarrays_max_sum += num

while max_stack and max_stack[-1][1] <= num:
_, prev_num, prev_shares = max_stack.pop()

max_shares += prev_shares # Max shares transition.

# Reflect transition in max sum.
subarrays_max_sum += (num - prev_num) * prev_shares

max_stack.append([end_idx, num, max_shares])

min_shares = 1 # Base case.
subarrays_min_sum += num

while min_stack and min_stack[-1][1] >= num:
_, prev_num, prev_shares = min_stack.pop()

min_shares += prev_shares # Min shares transition.

# Reflect transition in min sum.
subarrays_min_sum += (num - prev_num) * prev_shares

min_stack.append([end_idx, num, min_shares])

subarrays_max_min_sum += subarrays_max_sum + subarrays_min_sum

return subarrays_max_min_sum