232 lines
11 KiB
YAML
232 lines
11 KiB
YAML
title: Largest Rectangle in Histogram
|
|
slug: largest-rectangle-in-histogram
|
|
difficulty: hard
|
|
leetcode_id: 84
|
|
leetcode_url: https://leetcode.com/problems/largest-rectangle-in-histogram/
|
|
categories:
|
|
- arrays
|
|
- stack
|
|
patterns:
|
|
- slug: monotonic-stack
|
|
is_optimal: true
|
|
|
|
function_signature: "def largest_rectangle_area(heights: list[int]) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { heights: [2, 1, 5, 6, 2, 3] }
|
|
expected: 10
|
|
- input: { heights: [2, 4] }
|
|
expected: 4
|
|
- input: { heights: [1, 1] }
|
|
expected: 2
|
|
hidden:
|
|
- input: { heights: [1] }
|
|
expected: 1
|
|
- input: { heights: [0] }
|
|
expected: 0
|
|
- input: { heights: [2, 1, 2] }
|
|
expected: 3
|
|
- input: { heights: [1, 2, 3, 4, 5] }
|
|
expected: 9
|
|
- input: { heights: [5, 4, 3, 2, 1] }
|
|
expected: 9
|
|
- input: { heights: [3, 6, 5, 7, 4, 8, 1, 0] }
|
|
expected: 20
|
|
- input: { heights: [4, 2, 0, 3, 2, 5] }
|
|
expected: 6
|
|
|
|
description: |
|
|
Given an array of integers `heights` representing the histogram's bar height where the width of each bar is `1`, return *the area of the largest rectangle in the histogram*.
|
|
|
|
constraints: |
|
|
- `1 <= heights.length <= 10^5`
|
|
- `0 <= heights[i] <= 10^4`
|
|
|
|
examples:
|
|
- input: "heights = [2,1,5,6,2,3]"
|
|
output: "10"
|
|
explanation: "The largest rectangle is formed using bars at indices 2 and 3 (heights 5 and 6), with width 2 and height 5, giving area = 10 units."
|
|
- input: "heights = [2,4]"
|
|
output: "4"
|
|
explanation: "The largest rectangle is the single bar of height 4 with width 1, giving area = 4 units."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're standing at each bar in the histogram, trying to figure out how far you can extend a rectangle horizontally while keeping that bar's height as the minimum.
|
|
|
|
For any bar at position `i`, the largest rectangle that uses this bar's height extends:
|
|
- **Left**: until we hit a bar shorter than `heights[i]`
|
|
- **Right**: until we hit a bar shorter than `heights[i]`
|
|
|
|
The area is then `height[i] * (right_boundary - left_boundary - 1)`.
|
|
|
|
The brute force approach would check every bar and scan left and right to find boundaries — but that's O(n^2). The key insight is that a **monotonic stack** can find these boundaries efficiently.
|
|
|
|
Think of it like this: as you scan left-to-right, maintain a stack of bar indices in **increasing order of height**. When you encounter a bar shorter than the stack's top, you've found the right boundary for all taller bars on the stack. Pop them off and calculate their areas — the new stack top gives you their left boundary.
|
|
|
|
This works because the stack invariant guarantees that for any bar we pop, all bars between its left boundary (the new stack top) and right boundary (current index) are at least as tall.
|
|
|
|
approach: |
|
|
We solve this using a **Monotonic Stack**:
|
|
|
|
**Step 1: Initialise variables**
|
|
|
|
- `stack`: Empty list to store indices of bars in increasing height order
|
|
- `max_area`: Set to `0` to track the largest rectangle found
|
|
|
|
|
|
|
|
**Step 2: Iterate through each bar**
|
|
|
|
- For each index `i` with height `heights[i]`:
|
|
- While the stack is non-empty AND the current height is less than the height at the stack's top index:
|
|
- Pop the top index as `height_idx` — this bar can't extend further right
|
|
- Calculate the width: if stack is empty, width is `i` (bar extends to the beginning); otherwise width is `i - stack[-1] - 1`
|
|
- Calculate area as `heights[height_idx] * width`
|
|
- Update `max_area` if this area is larger
|
|
- Push current index `i` onto the stack
|
|
|
|
|
|
|
|
**Step 3: Process remaining bars in the stack**
|
|
|
|
- After the loop, bars remaining in the stack extend all the way to the right end
|
|
- Pop each remaining index and calculate its area using `n` as the right boundary
|
|
- Width calculation: if stack becomes empty, width is `n`; otherwise width is `n - stack[-1] - 1`
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- Return `max_area`
|
|
|
|
|
|
|
|
The monotonic stack ensures each bar is pushed and popped at most once, giving O(n) time complexity.
|
|
|
|
common_pitfalls:
|
|
- title: Forgetting to Process Remaining Stack
|
|
description: |
|
|
After iterating through all bars, some indices may still be on the stack. These represent bars that extend all the way to the right edge of the histogram.
|
|
|
|
Forgetting to process these remaining bars will miss valid rectangles. For example, with `heights = [2, 4]`, after the loop the stack contains both indices. Without processing, you'd return `0` instead of `4`.
|
|
wrong_approach: "Only calculate areas during the main loop"
|
|
correct_approach: "Process remaining stack elements using array length as right boundary"
|
|
|
|
- title: Incorrect Width Calculation
|
|
description: |
|
|
When popping a bar from the stack, its left boundary isn't always the immediately preceding bar — it's the bar at the new stack top (or the start if stack is empty).
|
|
|
|
A common mistake is calculating width as `i - popped_index`, but this is wrong. The correct width is `i - stack[-1] - 1` (or just `i` if stack is empty).
|
|
|
|
For example, in `[1, 5, 6, 2]`, when we pop index 2 (height 6) at index 3, the width isn't `3 - 2 = 1`. The bar of height 6 can extend left to where height 5 is, so width is `3 - 1 - 1 = 1`. But when we pop index 1 (height 5), the width is `3 - 0 - 1 = 2`.
|
|
wrong_approach: "Width = current_index - popped_index"
|
|
correct_approach: "Width = current_index - new_stack_top - 1 (or current_index if stack empty)"
|
|
|
|
- title: Brute Force Time Limit Exceeded
|
|
description: |
|
|
The naive O(n^2) approach — for each bar, scan left and right to find boundaries — will cause TLE with `heights.length <= 10^5`.
|
|
|
|
10^5 elements means up to 10^10 operations, which is far too slow. The monotonic stack reduces this to O(n) by computing all boundaries in a single pass.
|
|
wrong_approach: "For each bar, scan left and right to find boundaries"
|
|
correct_approach: "Use monotonic stack to find boundaries in O(n)"
|
|
|
|
key_takeaways:
|
|
- "**Monotonic stack pattern**: When you need to find the next smaller/larger element for all positions, a monotonic stack provides O(n) efficiency"
|
|
- "**Width calculation insight**: The left boundary for a popped element is always the new stack top, not the previous element"
|
|
- "**Process the stack after iteration**: Elements remaining in the stack have implicit right boundaries at the array end"
|
|
- "**Foundation for related problems**: This technique extends to problems like Maximal Rectangle, Trapping Rain Water, and daily temperatures"
|
|
|
|
time_complexity: "O(n). Each bar index is pushed onto and popped from the stack at most once, giving 2n operations total."
|
|
space_complexity: "O(n). In the worst case (strictly increasing heights), all n indices are stored on the stack simultaneously."
|
|
|
|
solutions:
|
|
- approach_name: Monotonic Stack
|
|
is_optimal: true
|
|
code: |
|
|
def largest_rectangle_area(heights: list[int]) -> int:
|
|
stack = [] # Store indices of bars in increasing height order
|
|
max_area = 0
|
|
n = len(heights)
|
|
|
|
for i in range(n):
|
|
# Pop bars that can't extend further right
|
|
while stack and heights[i] < heights[stack[-1]]:
|
|
height_idx = stack.pop()
|
|
height = heights[height_idx]
|
|
# Width extends from new stack top to current index
|
|
width = i if not stack else i - stack[-1] - 1
|
|
max_area = max(max_area, height * width)
|
|
stack.append(i)
|
|
|
|
# Process remaining bars - they extend to the right edge
|
|
while stack:
|
|
height_idx = stack.pop()
|
|
height = heights[height_idx]
|
|
# Width extends from new stack top to array end
|
|
width = n if not stack else n - stack[-1] - 1
|
|
max_area = max(max_area, height * width)
|
|
|
|
return max_area
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Each index is pushed and popped at most once.
|
|
|
|
**Space Complexity:** O(n) — Stack may contain all indices in worst case.
|
|
|
|
We maintain a stack of indices in increasing height order. When we encounter a shorter bar, we pop taller bars and calculate their areas since they can't extend further right. The stack top after popping gives the left boundary.
|
|
|
|
- approach_name: Monotonic Stack with Sentinel
|
|
is_optimal: true
|
|
code: |
|
|
def largest_rectangle_area(heights: list[int]) -> int:
|
|
# Add sentinel bars: 0-height at start and end
|
|
heights = [0] + heights + [0]
|
|
stack = [0] # Stack starts with left sentinel
|
|
max_area = 0
|
|
|
|
for i in range(1, len(heights)):
|
|
while heights[i] < heights[stack[-1]]:
|
|
height = heights[stack.pop()]
|
|
# Width between current index and new stack top
|
|
width = i - stack[-1] - 1
|
|
max_area = max(max_area, height * width)
|
|
stack.append(i)
|
|
|
|
return max_area
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Same as basic approach.
|
|
|
|
**Space Complexity:** O(n) — Stack plus modified heights array.
|
|
|
|
Adding sentinel bars of height 0 at both ends eliminates edge case handling. The left sentinel ensures the stack is never empty, and the right sentinel forces all remaining bars to be processed. This leads to cleaner code at the cost of a slightly modified input array.
|
|
|
|
- approach_name: Brute Force
|
|
is_optimal: false
|
|
code: |
|
|
def largest_rectangle_area(heights: list[int]) -> int:
|
|
max_area = 0
|
|
n = len(heights)
|
|
|
|
for i in range(n):
|
|
height = heights[i]
|
|
# Find left boundary - first bar shorter than current
|
|
left = i
|
|
while left > 0 and heights[left - 1] >= height:
|
|
left -= 1
|
|
# Find right boundary - first bar shorter than current
|
|
right = i
|
|
while right < n - 1 and heights[right + 1] >= height:
|
|
right += 1
|
|
# Calculate area with current bar's height
|
|
width = right - left + 1
|
|
max_area = max(max_area, height * width)
|
|
|
|
return max_area
|
|
explanation: |
|
|
**Time Complexity:** O(n^2) — For each bar, we scan left and right.
|
|
|
|
**Space Complexity:** O(1) — Only tracking indices and area.
|
|
|
|
For each bar, we expand left and right while adjacent bars are at least as tall. This finds the maximum width rectangle using each bar's height. While correct, this approach is too slow for large inputs (TLE on LeetCode) because it re-scans the same regions repeatedly.
|