questions F-L

This commit is contained in:
2025-05-25 11:47:04 +01:00
parent 360b5fa255
commit ad320dc703
54 changed files with 11235 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
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:
- monotonic-stack
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
&nbsp;
**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
&nbsp;
**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`
&nbsp;
**Step 4: Return the result**
- Return `max_area`
&nbsp;
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.