A stack kept sorted answers “nearest element to the left/right that is smaller/greater” for every position in total. It is also the Cartesian tree in disguise, which explains why it shows up in RMQ, histograms and “sum over all subarrays” problems.
Nearest smaller / greater
vector<int> L(n), R(n), st; // L: previous strictly smaller, R: next smaller-or-equal
for (int i = 0; i < n; i++) {
while (!st.empty() && a[st.back()] >= a[i]) R[st.back()] = i, st.pop_back();
L[i] = st.empty() ? -1 : st.back();
st.push_back(i);
}
for (int i : st) R[i] = n;One pass gives both sides: an index is popped by its next smaller-or-equal element, and when pushed it reads its previous strictly smaller one. Flip the comparisons for “greater”. In general: for “previous”, read st.back() before pushing the current index; for “next”, assign the answer while popping.
| Want | Direction | Pop while |
|---|---|---|
| previous smaller | left to right | a[st.back()] >= a[i] |
| previous greater | left to right | a[st.back()] <= a[i] |
| next smaller | right to left (or one pass, as above) | a[st.back()] >= a[i] |
| next greater | right to left (or one pass) | a[st.back()] <= a[i] |
Ties: strict on one side and non-strict on the other means every subarray has exactly one designated minimum (the leftmost or rightmost occurrence). With strict on both sides, equal elements double count.
Proof: the one-pass stack is correct and
Invariant. After processing index (before the next iteration), the stack holds, bottom to top, exactly the indices whose value has not yet been exceeded or matched by anything to their right so far — equivalently, a strictly increasing subsequence of values (since anything already got popped when arrived). Why
L[i]is correct: at the moment is pushed, every index already popped (by or earlier) had value the value that popped it, so it cannot be ‘s previous-smaller; the stack top at that instant is therefore the nearest index to the left whose value is strictly less than — exactlyL[i]. WhyR[st.back()]is correct when pops it: is the first index encountered (scanning left to right) with , since every index between the popped one and was itself (else it would have popped it first) — so is the next smaller-or-equal. Complexity: each index is pushed exactly once and popped at most once, so thewhileloop runs at most times in total across the whole scan, giving .
Contribution: sums over all subarrays
is the minimum of subarrays:
Proof: the contribution formula
A subarray (with ) has as its minimum (under the tie-breaking convention above, which guarantees a unique witness index per subarray) iff every element in is and every element in is (or the strict/non-strict sides matching whichever convention was fixed) — i.e. iff and , since is the nearest index with a smaller-or-not-dominated value and the nearest with a value that would break the minimum. These two ranges are independent, giving choices for and for , so is the designated minimum of exactly subarrays. Since every subarray has exactly one designated minimum (by the tie-breaking rule), summing times its count over all counts every subarray’s minimum exactly once, proving .
- Sum of ranges (): the maximum version minus the minimum version.
- Count subarrays where is the max and the subarray satisfies something else (for example length , or sum constraints): the range of valid is the rectangle , so count it with arithmetic or with small-to-large over the shorter side (iterating the shorter side gives total, the “divide at the maximum” trick).
- Sum of gcds or ANDs over all subarrays: the set of distinct values ending at has size (Subarray Aggregates).
Largest rectangle in a histogram
The best rectangle with bar as its shortest spans , so the answer is . In one pass, with a sentinel:
Proof: the histogram formula and its one-pass computation
Formula. Fix and consider the rectangle of height that is as wide as possible while staying inside the histogram. It can extend left until hitting a bar shorter than (at ) and right until hitting one shorter (at ), so its width is and its area . Every candidate optimal rectangle has some bar as its shortest (the rectangle’s height is the min height over its span), and for that bar , the rectangle must lie within — so it is no larger than the maximal rectangle with bar as the limiting height. Hence the true optimum equals . One-pass correctness. When pops bar off the stack (because ), every bar between and was (else it would have popped first), so is exactly ; and the new stack top after popping is the nearest remaining bar with height scanning left — exactly — since everything between it and was and already popped. So both boundaries of ‘s maximal rectangle are known at the instant of popping, letting the single pass compute correctly for every ; the sentinel height forces every remaining bar to pop and be accounted for.
Tested against an brute force (for each bar, extend both directions linearly) on random height arrays.
ll largestRectangle(vector<ll> h) {
h.push_back(-1); vector<int> st; ll best = 0;
for (int i = 0; i < sz(h); i++) {
while (!st.empty() && h[st.back()] >= h[i]) {
ll ht = h[st.back()]; st.pop_back();
int left = st.empty() ? -1 : st.back();
best = max(best, ht * (i - left - 1));
}
st.push_back(i);
}
return best;
}Maximal all-ones rectangle in a grid: for each row, heights = consecutive ones ending at that row; run the histogram per row, . Count all-ones subrectangles: per row, the number of rectangles with bottom-right corner at satisfies where is the previous strictly smaller height. in total.
Divide-and-conquer alternative: find the minimum bar; the answer either spans the whole range at that height or lies entirely to one side. With a sparse table for the minimum’s index this is , and if the Cartesian tree below is built first, since the recursion is that tree — slower to set up than the stack, but it’s the clearest explanation of why the Cartesian tree and the monotonic stack solve the same problem.
Trapping rain water
Water at is . Two pointers: move the side with the smaller current height, since its running maximum is the binding wall.
Proof: moving the shorter side is safe
Let and be the code’s running maxima (each includes the current pointer’s own bar once absorbed), and let be the true right maximum at position — the quantity the water formula needs but the algorithm never computes directly. Invariant: whenever , and . Base case (, both maxima ): trivial. Inductive step, moving (case ): the new since both (by hypothesis) and are at most that range’s max. The window shrinks to ; if then , so stays within the new, smaller range’s max. The only risk is — but puts inside that range, so it would force , contradicting the case hypothesis . So the invariant persists; the symmetric argument covers moving . Conclusion: right before adding water at (when ), the invariant gives , so regardless of the true (unknown to the algorithm) value of — the water formula reduces to , exactly what the code adds, with no need to ever learn .
ll trap(const vector<int>& h) {
int l = 0, r = sz(h) - 1, lm = 0, rm = 0; ll w = 0;
while (l < r)
if (h[l] < h[r]) lm = max(lm, h[l]), w += lm - h[l++];
else rm = max(rm, h[r]), w += rm - h[r--];
return w;
}The stack version fills horizontal layers: pop a bottom, and the water above it is bounded by the new top and the current bar. 2D version: push all border cells into a min-heap and flood inward Dijkstra-style, since water on a grid escapes over the lowest point of the boundary. The current level is , and each cell traps . — the “water level” propagates inward exactly as Dijkstra’s distances do.
| Method | Time | Space | Note |
|---|---|---|---|
| brute force (max on each side per ) | the starting point | ||
| prefix/suffix maxima | clearest | ||
| two pointers | optimal | ||
| monotonic stack | layer decomposition |
| Variant | Method |
|---|---|
| 2D trapping rain water | priority queue from the border inward (Dijkstra-like flood) |
| container with most water | two pointers on |
| largest rectangle in a histogram | monotonic stack, see above |
| water with a leak / drainage | modify the boundary condition |
| maximum water after removing bars | harder; DP or greedy depending on the constraints |
Cartesian tree
The min-Cartesian tree of has the minimum at the root, the left part as the left subtree and the right part as the right subtree. The monotonic stack is its right spine during construction:
vector<int> par(n, -1), st;
for (int i = 0; i < n; i++) {
int last = -1;
while (!st.empty() && a[st.back()] > a[i]) last = st.back(), st.pop_back();
if (last != -1) par[last] = i;
if (!st.empty()) par[i] = st.back();
st.push_back(i);
}Proof: the stack construction builds the Cartesian tree
The stack always holds, bottom to top, the right spine of the tree built from so far (the path from the root following only rightmost children), with values increasing bottom to top — this holds inductively since the spine’s values must increase downward for the min-heap property to hold along a chain of right-children. When arrives, every stack entry with value must move below in the final tree (min-heap property), and they are popped from the top of the stack down, i.e. from the shallowest spine node down to the deepest one still exceeding — call the last one popped
last. Re-rooting that whole popped chain at as a single left subtree is correct becauselastpreviously had no right child (it was the bottom of the old spine), so attachingpar[last]=islots above exactly that chain, preserving both the heap property (every popped value ) and the BST property (every popped index , since the stack only ever holds indices in increasing left-to-right order). After popping, if the stack is non-empty its top has the first remaining value and necessarily a larger index than any popped node but still , so attaching as its right child (par[i]=st.back()) extends the spine correctly; pushing then makes it the new spine bottom. Induction on shows this reconstructs the heap-by-value, BST-by-index Cartesian tree exactly, and since each index is pushed once and popped at most once, the construction is .
- in the Cartesian tree, giving the / RMQ (RMQ).
- “Divide at the minimum” recursions are traversals of this tree. Merging the smaller child into the larger gives for many “count subarrays whose minimum (or maximum) satisfies ” problems.
- A treap is a Cartesian tree on (key, random priority).
More stack patterns
| Problem | Idea |
|---|---|
| stock span, days until warmer | previous/next greater, distance |
| lexicographically smallest subsequence of length / remove digits | pop while the top is larger and enough elements remain |
| smallest subsequence containing every distinct letter once | pop while top > current and the top occurs later |
| circular next greater | iterate positions, push only in the first pass |
| visible people in a queue / count pairs that see each other | pop smaller, count pops, handle equal heights with counts |
| sum of of all subarrays | two stacks, contribution |
| convex hull (monotone chain), CHT with sorted slopes | same pop-while-bad discipline with a cross product |
| matching brackets, longest valid parentheses | stack of indices with a sentinel base |
| next greater for queries offline / on trees | stack along the DFS path with rollback |
Debugging
The stack holds exactly the indices still “open”, whose answer hasn’t been determined. If a result is wrong, print the stack after each step on a 6-element example. Nearly every bug is < versus <=, or reading the top after pushing instead of before.