View on NeetCode
View on LeetCode

Problem

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.

Example 1:

Input: heights = [2,1,5,6,2,3]
Output: 10
Explanation: The above is a histogram where width of each bar is 1.
The largest rectangle is shown in the red area, which has an area = 10 units.

Example 2:

Input: heights = [2,4]
Output: 4

Constraints:

  • 1 <= heights.length <= 10^5
  • 0 <= heights[i] <= 10^4

Solution

Approach: Monotonic Increasing Stack

The key insight is to use a stack to find, for each bar, the maximum width of a rectangle with that bar as the minimum height. We maintain a monotonically increasing stack of indices.

For each bar:

  1. While the current bar is shorter than the bar at the top of the stack, we’ve found the right boundary for that taller bar
  2. Pop the taller bar and calculate its maximum rectangle area
  3. Push the current bar’s index

The left boundary is the previous element in the stack (or 0 if stack is empty), and the right boundary is the current index.

Implementation

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        max_area = 0
        stack = []  # Stack stores (index, height)

        for i, h in enumerate(heights):
            start = i
            # Pop all bars taller than current
            while stack and stack[-1][1] > h:
                index, height = stack.pop()
                # Width = current index - popped bar's start index
                max_area = max(max_area, height * (i - index))
                start = index  # Current bar can extend back to this index

            stack.append((start, h))

        # Process remaining bars in stack
        for index, height in stack:
            max_area = max(max_area, height * (len(heights) - index))

        return max_area

Alternative (Indices Only):

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        max_area = 0
        stack = []  # Stack stores indices

        for i in range(len(heights)):
            # Pop taller bars and calculate their areas
            while stack and heights[stack[-1]] > heights[i]:
                h_index = stack.pop()
                height = heights[h_index]
                # Width: from element after previous stack top to current
                width = i if not stack else i - stack[-1] - 1
                max_area = max(max_area, height * width)

            stack.append(i)

        # Process remaining bars
        while stack:
            h_index = stack.pop()
            height = heights[h_index]
            width = len(heights) if not stack else len(heights) - stack[-1] - 1
            max_area = max(max_area, height * width)

        return max_area

Complexity Analysis

  • Time Complexity: O(n), where n is the number of bars. Each bar is pushed and popped from the stack at most once.
  • Space Complexity: O(n) in the worst case, when heights are in increasing order and we push all indices onto the stack.

Key Insights

  1. Monotonic Stack: We maintain a stack of indices where heights are in increasing order. When we encounter a shorter bar, it acts as the right boundary for all taller bars in the stack.

  2. Width Calculation: For a bar at index i popped when processing index j, the width is j - i (or adjusted based on the previous stack element). The bar can extend left to where a shorter bar exists.

  3. Start Index Extension: When we pop a taller bar, the current shorter bar can extend back to where the taller bar started. This is why we track the start index.

  4. Remaining Bars: Bars still in the stack after processing all elements can extend to the end of the histogram. We process them with width = len(heights) - index.

  5. Why This Works: For each bar, we find the maximum width rectangle where that bar is the minimum height. The monotonic stack efficiently finds both left and right boundaries where shorter bars exist.