Problem overview
You get an IntArray heights, the heights of the bars in a histogram. Every bar is 1 unit wide. The function returns the area of the largest rectangle that fits inside the histogram, where the rectangle is made of neighbouring bars and its height is limited by the shortest bar it covers.
For [2, 1, 5, 6, 2, 3] the answer is 10: the bars of heights 5 and 6 are neighbours, so a rectangle of height 5 and width 2 fits over them, and 5 x 2 = 10. For [2, 4] the answer is 4, from either the single bar of height 4 or the rectangle of height 2 and width 2.
The constraints allow up to 10^5 bars with heights up to 10^4, so the algorithm has to be close to linear.
Approach
Every rectangle is limited by its shortest bar. That gives a way to check all candidates without listing them: take each bar in turn and treat it as the shortest bar of the rectangle. The widest rectangle of height heights[i] stretches left and right until it reaches a bar that is lower than heights[i]. If you know
left[i], the index of the nearest lower bar to the left, andright[i], the index of the nearest lower bar to the right,
then the rectangle covers the bars strictly between them. Its width is right[i] - left[i] - 1 and its area is heights[i] * (right[i] - left[i] - 1). The answer is the maximum over all i.
Checking every bar by walking left and right costs O(n^2) in the worst case. The nearest lower bar is the same kind of question as "next greater element", and a stack answers it for all bars in a single pass, O(n) in total.
Step by step
- The size.
nholdsheights.size, since the code uses it many times. - The left answers.
leftis anIntArray(n)filled with-1. The value-1means "no lower bar on the left", as if a bar of height 0 stood just before the first index. - The right answers.
rightis filled withn, which means "no lower bar on the right": a virtual bar of height 0 just past the end.ncan't be confused with a real index. - The stack.
stackis anArrayDeque<Int>that holds the indices of bars still waiting for their nearest lower bar. Their heights never decrease from bottom to top. - Left-to-right pass. For each
ifrom 0 ton - 1, call the helperremoveAndUpdate(heights, stack, right, i). Scanning rightwards, the first lower bar an index meets is its nearest lower bar on the right, so the answers go intoright. - Reset.
stack.clear()empties the stack. Indices left on it in the first pass have no lower bar to their right, so their entries inrightstayn. The variable is aval, but its contents can still be cleared. - Right-to-left pass. Loop with
ifromn - 1downTo0and callremoveAndUpdate(heights, stack, left, i). Scanning leftwards, the same helper finds the nearest lower bar on the left. - Best area.
answerstarts at 0 and, for everyi, becomesmaxOf(answer, heights[i] * (right[i] - left[i] - 1)). - Return
answer.
The helper
removeAndUpdate sits inside the Solution class next to largestRectangleArea. Its parameters are the heights, the stack, the array to fill (arr, which is either left or right) and the index i:
- While the stack is not empty and
heights[i]is less than the height of the bar whose index is on top, bariis the first lower bar that the top index has met. - Set
arr[stack.last()] = i, then pop that index withremoveLast(). - When the loop ends, push
iwithaddLastso it waits for its own answer.
The loop pops only strictly taller bars, so bars of equal height stay on the stack. That's fine: an equal bar is not lower, so it doesn't stop a rectangle, and the rectangle may extend over it.
The solution in Kotlin
class Solution {
fun removeAndUpdate(heights: IntArray, stack: ArrayDeque<Int>, arr: IntArray, i: Int) {
while (stack.isNotEmpty() && heights[i] < heights[stack.last()]) {
arr[stack.last()] = i
stack.removeLast()
}
stack.addLast(i)
}
fun largestRectangleArea(heights: IntArray): Int {
val n = heights.size
val left = IntArray(n) { -1 }
val right = IntArray(n) { n }
val stack = ArrayDeque<Int>()
for (i in 0 until n) {
removeAndUpdate(heights, stack, right, i)
}
stack.clear()
for (i in n - 1 downTo 0) {
removeAndUpdate(heights, stack, left, i)
}
var answer = 0
for (i in 0 until n) {
answer = maxOf(answer, heights[i] * (right[i] - left[i] - 1))
}
return answer
}
}
Kotlin details worth knowing:
- Arrays and the deque are objects, and Kotlin passes references to them. The helper changes the caller's own
arrandstack, not copies. The helper has no return type, so it returnsUnit. - The same helper serves both directions because it does not care which way the loop runs: it only compares
heights[i]with the bars waiting on the stack. 0 until nexcludesn;n - 1 downTo 0counts down and includes both ends.IntArray(n) { n }uses the lambda to fill every position withn.- In the helper's loop condition,
stack.isNotEmpty()comes first so thatstack.last(), which throws on an empty deque, is never reached on an empty stack. - The area is an
Int. With the largest allowed input, 10^5 bars of height 10^4, it is 10^9, which fits within theIntlimit of about 2.1 billion.
Complexity
Time: O(n). Each of the two passes pushes every index once and pops it at most once, so each pass is O(n). The final loop is O(n) as well.
Space: O(n). The left and right arrays and the stack each take up to n entries.
Edge cases and common mistakes
- A single bar. For
[0]the answer is 0; for[7],left[0]is -1 andright[0]is 1, so the width is 1 and the area is 7. - Equal bars. For
[3, 3, 3]no bar is lower than another, so every bar gets the full width 3 and the answer is 9. - Off-by-one in the width. The nearest lower bars are not part of the rectangle, which is why the width is
right[i] - left[i] - 1. - Wrong defaults. The values -1 and
nstand for imaginary bars of height 0 outside the array. Using 0 andn - 1would shrink the width of rectangles that reach the edges. - Forgetting
stack.clear(). Indices from the first pass would leak into the second pass and produce wrongleftvalues.
Testing
[2, 1, 5, 6, 2, 3]returns 10.[2, 4]returns 4.[3, 3, 3]returns 9.[0]returns 0.