Find the largest axis-aligned rectangle that fits inside a histogram of bars with given heights. A monotonic increasing stack lets you compute, in a single pass, the farthest each bar can extend to the left — turning an O(n²) brute force into O(n).
Given an array heights where heights[i] is the height of the i-th bar (width 1), return the area of the largest rectangle that fits entirely within the histogram.
Example: heights = [2,1,5,6,2,3]. The largest rectangle uses bars at indices 2 and 3 (heights 5 and 6), both limited to height 5, giving area 5 × 2 = 10. Answer: 10.
The tricky part: a rectangle can span multiple bars but is limited to the shortest bar in that span. You need to try all possible heights efficiently.
[startIndex, height] pairs in strictly increasing order of height. The start index records how far left this bar can still extend (it may be farther than its actual column).i from 0 to n inclusive. At i = n use a synthetic height of 0 so every remaining stack entry gets flushed without special-casing.>= curH, pop it. The popped bar spans from its recorded startIndex to i - 1, giving width = i - startIndex. Update maxArea if h × width is larger. Also set start = startIndex — the current bar inherits this left boundary because it was unblocked all along.[start, curH] onto the stack. The start may be to the left of i if we popped any taller bars.maxArea.The stack holds at most n entries (one per bar) so space is O(n). There is no O(1)-space solution known for the general case — the stack is essential. The sentinel trick (appending a virtual height-0 bar) cleanly avoids a post-loop flush and is idiomatic.
2, 1, 5, 6, 2, 3. We scan left to right, maintaining a monotonic increasing stack of (index, height) pairs.1function largestRectangleArea(heights: number[]): number {2 // Stack stores [startIndex, height] pairs.3 // Invariant: heights in the stack are always strictly increasing.4▶ const stack: [number, number][] = [];5▶ let maxArea = 0;67 for (let i = 0; i <= heights.length; i++) {8 const curH = i < heights.length ? heights[i] : 0; // sentinel 0 flushes all9 let start = i;1011 while (stack.length > 0 && stack[stack.length - 1][1] >= curH) {12 const [idx, h] = stack.pop()!;13 const width = i - idx;14 maxArea = Math.max(maxArea, h * width);15 start = idx; // this bar's left boundary extends back to here16 }1718 stack.push([start, curH]);19 }2021 return maxArea;22}
function largestRectangleArea(heights: number[]): number {
// Stack stores [startIndex, height] pairs.
// Invariant: heights in the stack are always strictly increasing.
const stack: [number, number][] = [];
let maxArea = 0;
for (let i = 0; i <= heights.length; i++) {
const curH = i < heights.length ? heights[i] : 0; // sentinel 0 flushes all
let start = i;
while (stack.length > 0 && stack[stack.length - 1][1] >= curH) {
const [idx, h] = stack.pop()!;
const width = i - idx;
maxArea = Math.max(maxArea, h * width);
start = idx; // this bar's left boundary extends back to here
}
stack.push([start, curH]);
}
return maxArea;
}[startIndex, height]pairs. The start index is the leftmost column this bar's height can still cover. maxArea accumulates the running best.heights.length inclusive lets us treat index n as a virtual bar of height 0. This forces every remaining stack entry to pop and get measured without needing a separate post-loop pass.width = i - idx(the popped bar's left boundary), and update the max. Record start = idx so the current bar inherits the left boundary of the last popped entry — the rectangle of the current bar extends that far back too.[start, curH]. If we popped any bars, start is to the left of i, encoding that this bar's potential rectangle begins there.maxAreaholds the answer. Every possible "limiting height" bar has been considered exactly once.while loop looks O(k) per iteration, but each bar is pushed exactly once and popped at most once, so the total work across all iterations is O(n). O(n) space for the stack.h × n. The algorithm handles it correctly; just verify with [3,3,3] → 9.dp[j] = number of consecutive filled cells ending at the current row in column j. Call largestRectangleArea(dp) for each row.bestL, bestR, and bestH whenever you update maxArea.minH, compute area — O(n²). The stack removes the need to rescan by recording each bar's left boundary at push time.function largestRectangleArea(heights: number[]): number {
// Stack stores [startIndex, height] pairs.
// Invariant: heights in the stack are always strictly increasing.
const stack: [number, number][] = [];
let maxArea = 0;
for (let i = 0; i <= heights.length; i++) {
const curH = i < heights.length ? heights[i] : 0; // sentinel 0 flushes all
let start = i;
while (stack.length > 0 && stack[stack.length - 1][1] >= curH) {
const [idx, h] = stack.pop()!;
const width = i - idx;
maxArea = Math.max(maxArea, h * width);
start = idx; // this bar's left boundary extends back to here
}
stack.push([start, curH]);
}
return maxArea;
}while loop looks O(k) per iteration, but each bar is pushed exactly once and popped at most once, so the total work across all iterations is O(n). O(n) space for the stack.Precompute, for each bar, the index of the nearest strictly-shorter bar to its left and to its right, then every bar's maximal width is just the gap between those two walls.
function largestRectangleArea(heights: number[]): number {
const n = heights.length;
if (n === 0) return 0;
// left[i] = index of nearest bar to the LEFT that is strictly shorter than heights[i];
// -1 if no such bar exists.
const left: number[] = new Array<number>(n);
// right[i] = index of nearest bar to the RIGHT that is strictly shorter; n if none.
const right: number[] = new Array<number>(n);
// Left pass: a monotonic stack of indices with strictly increasing heights.
const leftStack: number[] = [];
for (let i = 0; i < n; i++) {
while (leftStack.length > 0 && heights[leftStack[leftStack.length - 1]] >= heights[i]) {
leftStack.pop();
}
left[i] = leftStack.length === 0 ? -1 : leftStack[leftStack.length - 1];
leftStack.push(i);
}
// Right pass: same idea, scanning from the right.
const rightStack: number[] = [];
for (let i = n - 1; i >= 0; i--) {
while (rightStack.length > 0 && heights[rightStack[rightStack.length - 1]] >= heights[i]) {
rightStack.pop();
}
right[i] = rightStack.length === 0 ? n : rightStack[rightStack.length - 1];
rightStack.push(i);
}
// Each bar limits a rectangle spanning (left[i], right[i]) exclusive on both ends.
let maxArea = 0;
for (let i = 0; i < n; i++) {
const width = right[i] - left[i] - 1;
maxArea = Math.max(maxArea, heights[i] * width);
}
return maxArea;
}>= when popping) avoids double-counting equal-height plateaus.Treat every bar as the limiting height and physically walk outward in both directions while the neighbouring bars are at least as tall.
function largestRectangleArea(heights: number[]): number {
const n = heights.length;
let maxArea = 0;
for (let i = 0; i < n; i++) {
const h = heights[i];
// Expand left while bars stay at least as tall as h.
let lo = i;
while (lo - 1 >= 0 && heights[lo - 1] >= h) {
lo--;
}
// Expand right while bars stay at least as tall as h.
let hi = i;
while (hi + 1 < n && heights[hi + 1] >= h) {
hi++;
}
const width = hi - lo + 1;
maxArea = Math.max(maxArea, h * width);
}
return maxArea;
}| "largest / maximum rectangle in histogram" | monotonic increasing stack |
| each element is the bottleneck height for some subarray | pop when shorter arrives, compute width |
| "maximal rectangle in binary matrix" | histogram row-by-row + LC 84 |
| next smaller element with area calculation | stack stores [startIdx, height] |
const stack: [number, number][] = []; // [startIndex, height]
let maxArea = 0;
for (let i = 0; i <= heights.length; i++) {
const curH = i < heights.length ? heights[i] : 0;
let start = i;
while (stack.length > 0 && stack[stack.length - 1][1] >= curH) {
const [idx, h] = stack.pop()!;
maxArea = Math.max(maxArea, h * (i - idx));
start = idx;
}
stack.push([start, curH]);
}
return maxArea;[startIndex, height]. The start index is the leftmost column this bar's height can still cover — it may be to the left of the bar's actual column because taller bars were popped out of the way.heights = [2,1,5,6,2,3], what is the largest rectangle area?0 appended at index n?startIndex = 2 at current index i = 5 and height h = 4, what area do we compute?largestRectangleArea once per row?