Problem overview
The method largestRectangleArea receives an Array[Int] named heights. Each element is the height of a bar, and every bar is one unit wide. The method returns the area of the largest rectangle that fits inside the bars, where the rectangle's sides run along the bars and its base sits on the axis.
For heights = Array(2, 1, 5, 6, 2, 3) the answer is 10. The bars at indices 2 and 3 have heights 5 and 6, so a rectangle of height 5 and width 2 fits over them: 5 * 2 = 10. For Array(2, 4) the answer is 4, either as a rectangle of height 4 and width 1 or as one of height 2 and width 2.
The input can have up to 100,000 bars with heights up to 10,000, so a method that tries every pair of left and right ends, which takes O(n^2) time or worse, is too slow.
Approach: the bar that limits the rectangle
Every rectangle is limited by its shortest bar. So instead of choosing a left end and a right end, choose the limiting bar i and ask how far a rectangle of height heights(i) can stretch. It can stretch left and right as long as the neighbouring bars are at least as tall as bar i. It stops at the nearest bar on each side that is strictly lower.
That gives two numbers per bar: left(i), the index of the nearest lower bar on the left, and right(i), the index of the nearest lower bar on the right. The widest rectangle of height heights(i) covers the indices strictly between them, so its width is right(i) - left(i) - 1. The answer is the maximum of heights(i) * (right(i) - left(i) - 1) over all bars.
Finding the nearest lower bar for every index could cost O(n) each, which would be O(n^2) in total. A stack does it in O(n) overall. While scanning from left to right, the stack holds the indices of bars that have not met a lower bar yet. Their heights never decrease from bottom to top. When bar i arrives, every bar on top of the stack that is taller than heights(i) has just found its nearest lower bar on the right: it is i. Pop those bars, record i for them, then push i. A scan from right to left with the same code finds the nearest lower bar on the left.
Step by step
- Store the length in
n, since it is used many times. - Create
leftwithArray.fill(n)(-1). The value -1 means "no lower bar on the left", as if a bar of height 0 stood before index 0. - Create
rightwithArray.fill(n)(n). The valuenmeans "no lower bar on the right", as if a bar of height 0 stood after the last index. The formula for the width then works at both borders without special cases. - Create the stack as an empty
mutable.Stack[Int]. - Scan left to right and call
removeAndUpdate(heights, stack, right, i)for each index. Bars that are still on the stack at the end have no lower bar to their right, so their entries inrightkeep the valuen. - Clear the stack so the second scan starts from nothing.
- Scan right to left over
(0 until n).reverseand call the same helper withleft. The helper doesn't care about the direction: it comparesheights(i)with the bars waiting on the stack. - Compute the answer. Start a
var answerat 0 and, for each index, keep the larger of the current answer andheights(i) * (right(i) - left(i) - 1). - Return
answeras the last expression. - The helper
removeAndUpdateloops while the stack is not empty andheights(i)is less than the height of the bar on top. In each pass it writesiintoarrfor the top index, which is the nearest lower bar of that index, and pops it. After the loop it pushesi.
The solution in Scala
class Solution {
def removeAndUpdate(heights: Array[Int], stack: mutable.Stack[Int], arr: Array[Int], i: Int): Unit = {
while (stack.nonEmpty && heights(i) < heights(stack.top)) {
arr(stack.top) = i
stack.pop()
}
stack.push(i)
}
def largestRectangleArea(heights: Array[Int]): Int = {
val n = heights.length
val left = Array.fill(n)(-1)
val right = Array.fill(n)(n)
val stack = mutable.Stack[Int]()
for (i <- 0 until n) {
removeAndUpdate(heights, stack, right, i)
}
stack.clear()
for (i <- (0 until n).reverse) {
removeAndUpdate(heights, stack, left, i)
}
var answer = 0
for (i <- 0 until n) {
answer = math.max(answer, heights(i) * (right(i) - left(i) - 1))
}
answer
}
}
For heights = Array(2, 1, 5, 6, 2, 3) the two arrays end up as:
left = -1, -1, 1, 2, 1, 4
right = 1, 6, 4, 4, 6, 6
For bar 2 (height 5), the nearest lower bars are at indices 1 and 4, so the width is 4 - 1 - 1 = 2 and the area is 10. Bar 1 (height 1) reaches both borders: its width is 6 - (-1) - 1 = 6, so its area is 6. The maximum over all six bars is 10.
Scala details:
Array.fill(n)(value)has two parameter lists. The first gives the size and the second gives the element. The element expression is evaluated once per slot, which doesn't matter for a constant.mutable.Stackis a mutable collection. The short name relies onimport scala.collection.mutableat the top of the file.pushadds on top,topreads the top without removing it, andpop()removes and returns it. Callingtopon an empty stack throws an exception, which is why the loop condition testsstack.nonEmptyfirst:&&stops at the first false operand.- The helper changes the caller's objects. Arrays and the stack are objects, so
removeAndUpdatewrites into the caller's ownrightorleftand stack, not into copies. That is why it can returnUnit. (0 until n).reversegives the indicesn - 1down to 0. The range0 until nexcludesn.- Last expression. The method ends with
answerand needs noreturn. - No overflow.
Intis 32 bits and wraps silently, but the largest possible area is 10,000 * 100,000 = 1,000,000,000, which fits below 2,147,483,647.
Complexity
Time: O(n). Each index is pushed once and popped at most once in each scan, so the two scans cost O(n) in total even though the helper contains a loop. The final loop is O(n) too.
Space: O(n). The arrays left and right and the stack each hold at most n numbers.
Edge cases and common mistakes
- A single bar.
Array(5)gives 5. Nothing is lower, soleft(0)is -1,right(0)is 1, and the width is 1 - (-1) - 1 = 1. - A bar of height 0.
Array(0, 9, 0)gives 9. The zero bars split the histogram, and a zero-height bar contributes an area of 0. - Equal heights.
Array(3, 3, 3)gives 9. The loop pops only strictly taller bars, so equal bars stay on the stack andleftandrightkeep their border values. Every bar then computes the full width of 3. Popping on<=instead would record an equal neighbour as a "lower" bar and cut rectangles short, so keep the strict comparison. - Sorted heights.
Array(1, 2, 3, 4, 5)andArray(5, 4, 3, 2, 1)both give 9, from 3 * 3. - Wrong border values. The width formula
right(i) - left(i) - 1assumes -1 andnas the "none" values. Startingleftat 0 orrightatn - 1makes bars that reach a border one unit too narrow. - Not clearing the stack between the scans leaves old indices behind, and the second scan then compares against bars it should never see.
Testing
Array(2, 1, 5, 6, 2, 3)returns 10.Array(2, 4)returns 4.Array(2, 1, 2)returns 3, from the bar of height 1 spanning all three positions.Array(3, 3, 3)returns 9.- An array of 100,000 elements all equal to 10,000 returns 1,000,000,000.
