bigonotes
DSA: Patterns & PythonStack

Monotonic Stack: Contribution, Span & Area

Use a monotonic stack to find how far each element reaches left and right, then sum its contribution, span or rectangle area.

5 min readLast updated

The next greater/smaller template tells you where each element's influence ends. Use it on both sides and you know the whole range where an element is the minimum (or maximum). That range answers a family of harder questions: the sum of all subarray minimums, a stock's price span, and the largest rectangle in a histogram.

The idea#

The trick is to flip the question from "for each subarray, what's its minimum?" (O(n2)O(n^2) subarrays) to "for each element, in how many subarrays is it the minimum?"

Say arr[i] stays the minimum for left choices of start position (itself plus the larger elements just before it) and right choices of end position. Every start can pair with every end, so arr[i] is the minimum of left * right subarrays and contributes left * arr[i] * right to the total.

  • left = distance back to the previous smaller element.
  • right = distance forward to the next smaller element.

Both come from one monotonic-stack pass each, so the whole sum is O(n)O(n).

Duplicates: break ties on one side only#

There is one catch. If the array has equal values, a subarray like [2, 2] has two candidates for "the minimum". Using strict "smaller" on both sides counts it for both 2s; using "smaller or equal" on both sides counts it for neither. Fix it asymmetrically:

  • On the left, stop at an element that is smaller or equal (<=).
  • On the right, stop only at one that is strictly smaller (<).

Now each subarray belongs to exactly one of the equal minimums: the leftmost one. The rightmost 2 can't reach back past the first 2, but the first 2 can reach forward over the second.

How to spot it#

The question sums or maximizes something over every subarray or every range. Look for these signals.

  • "Sum of (min / max) over all subarrays".
  • "For each element, how many consecutive previous days were lower or equal?" (a span).
  • "Largest rectangle" made of adjacent bars, where the shortest bar sets the height.
  • A brute force that, for each element, expands left and right while the neighbors are at least as large.

Template#

Two passes find each element's reach. The left pass pops on >, so the top is the previous smaller-or-equal. The right pass pops on >=, so the top is the next strictly smaller.

stack = empty
for i from left to right:
    while stack and value at top > arr[i]: pop
    left[i] = i - top if stack else i + 1
    push i
stack = empty
for i from right to left:
    while stack and value at top >= arr[i]: pop
    right[i] = top - i if stack else n - i
    push i
total = sum of left[i] * arr[i] * right[i]

One pass: score on pop#

The two-pass version is easy to read, but you can also do it in one pass by scoring an element at the moment it's popped. Its range can't continue past the element that popped it. Keep (start, value) pairs in increasing order.

for each index i with value v:
    start = i
    while stack and value at top > v:
        pop (s, sv)
        score sv over the range s .. i - 1
        start = s                 # v fits under everything popped
    push (start, v)
for each (s, sv) left on the stack:
    score sv over the range s .. n - 1   # never blocked on the right

The key line is start = s. The new, smaller value fits under everything it popped, so it inherits the leftmost start. Entries left at the end were never blocked, so their range runs to n.

Pitfalls#

Most bugs here either change the count or lose width. Watch for these.

  • Double counting equal minimums. Use <= on one side and < on the other, never the same on both.
  • Default reach. If nothing blocks on the left, left = i + 1 (every start from 0); if nothing blocks on the right, right = n - i.
  • Forgetting the leftovers. In the one-pass shape, entries still on the stack after the loop must be scored with a range that ends at n - 1.
  • Pushing the current index instead of the inherited start. Pushing (i, v) after popping loses the range to the left.

Problems in this pattern#

These problems use this pattern. Solve them before you open the hints below.

Hints#

Open a hint only after you have tried the problem for a while.

Hint: Online Stock Span

Keep (price, span) on the stack. When a new price arrives, pop every entry with price <= it and add their spans to today's span, which starts at 1. A popped day already knew how many days it covered, so you never walk back day by day.

Hint: Sum of Subarray Minimums

This is the two-pass template exactly. Mind the tie-break: [2, 2] should give 6 ([2], [2], [2, 2]). Return the result modulo 109+710^9 + 7.

Hint: Largest Rectangle in Histogram

Each bar is the height of the rectangle in which it's the shortest. Use the one-pass shape with (start, height) pairs. A popped bar scores height * (i - start), and leftovers score height * (n - start).

Hint: Maximal Rectangle

Turn each row into a histogram: the height of a column is the number of consecutive 1s ending at that row. Run Largest Rectangle in Histogram on every row.

Key takeaways#

  • Ask "in how many subarrays is this element the min?" instead of looping over subarrays.
  • Contribution = left * value * right, from previous and next smaller.
  • Break ties asymmetrically (<= left, < right) so equal values count once.
  • Spans add up: a popped entry hands its span to the one that popped it.
  • In the histogram, the popped bar is scored and the new bar inherits its start.
Revision notes · 2 min

Core idea#

Ask "in how many subarrays is this element the minimum?" instead of looping over subarrays. Monotonic stacks find each element's reach left and right in O(n)O(n).

Key points#

  • left is the distance back to the previous smaller element. right is the distance forward to the next smaller element.
  • The element is the minimum of left * right subarrays and contributes left * value * right.
  • Break ties on one side only. Stop at <= on the left and at strict < on the right.
  • Each subarray with equal minimums then belongs to the leftmost one.
  • Default reach: left = i + 1 and right = n - i when nothing blocks.
  • Spot it: "sum of min or max over all subarrays", a stock span, "largest rectangle" of adjacent bars.

Variants#

Problem Stack entry On pop
Sum of Subarray Minimums index set left / right reach
Online Stock Span (price, span) pop prices <= today; add their spans
Largest Rectangle in Histogram (start, height), increasing score height * (i - start); new bar inherits the popped start

Template#

stack = empty
for i from left to right:
    while stack and value at top > arr[i]: pop
    left[i] = i - top if stack else i + 1
    push i
stack = empty
for i from right to left:
    while stack and value at top >= arr[i]: pop
    right[i] = top - i if stack else n - i
    push i
total = sum of left[i] * arr[i] * right[i]

For Sum of Subarray Minimums, return the result modulo 109+710^9 + 7.

Pitfalls#

  • Same comparison on both sides. Strict on both double-counts; <= on both counts nothing.
  • Forgetting leftover bars in the histogram. Score them with width n - start.
  • Pushing (i, h) after popping. Push the inherited start, or you lose width to the left.

Say it in an interview#

  • "Each element contributes left times value times right, from the previous and next smaller."
  • "Break ties asymmetrically so equal values are counted once."

Found a mistake? Email ask@bigonotes.com