Problem overview
The method largestRectangleArea receives an array heights. Each number 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' edges.
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. For [2, 4] the answer is 4, either from the bar of height 4 alone or from both bars at height 2.
Approach: find the nearest lower bar on each side
Look at the biggest rectangle and its shortest bar. The rectangle can't be taller than that bar, and it can't be extended sideways past a bar that is lower. So every candidate rectangle can be described like this: pick a bar i as the limiting height, then stretch as far left and right as the neighbours stay at least as tall as heights[i].
That stretch is bounded by two indices:
left[i], the index of the nearest bar on the left that is lower thanheights[i], or-1if there is none;right[i], the index of the nearest lower bar on the right, ornif there is none.
The rectangle covers the bars strictly between them, so its width is right[i] - left[i] - 1 and its area is heights[i] * (right[i] - left[i] - 1). The answer is the largest such area over all bars.
Checking every bar by walking outwards until a lower bar appears costs O(n²) for a long staircase of bars. A stack makes it O(n). While scanning from left to right, the stack holds the indices of bars that haven't met a lower bar yet. Their heights never decrease from bottom to top. When a new bar is lower than the bar on top, that new bar is the first lower bar the top one has met, so the top bar's answer is known and it leaves the stack.
Take [2, 1, 5, 6, 2, 3]. Bar 0 (height 2) waits. Bar 1 (height 1) is lower, so bar 0 gets right[0] = 1, and bar 1 waits. Bars 2 and 3 are taller and join the stack. Bar 4 (height 2) is lower than 6 and 5, so right[3] = 4 and right[2] = 4; it stays above bar 1. Bar 5 joins. At the end, bars 1, 4 and 5 are still on the stack and keep the default right value, 6. A second pass from right to left, with the same helper, produces left: [-1, -1, 1, 2, 1, 4].
For bar 2 (height 5) the width is 4 - 1 - 1 = 2, and the area is 10.
Step by step
- Store the count in
n, since the bar count is needed many times. - Create
leftas avararray ofnvalues, all-1. It is avarbecause the helper will change it. - Create
rightwithnvalues, alln. The valuencan never be a real index, so it means "no lower bar". - Create the stack
stas an empty array ofInt. Its end is the top. - Scan left to right. Each bar gives the helper a chance to settle the bars waiting on the stack.
- Call the helper with
right, because scanning in this direction finds the first lower bar to the right of each waiting bar. - Start over with an empty stack for the second pass.
- Scan right to left with
stride(from: n - 1, through: 0, by: -1), which includes index 0. - Call the same helper with
left. Scanning from the right, the first lower bar a waiting index meets is its nearest lower bar on the left. - Keep the best area in
answer, starting at 0. - Try every bar as the limiting height and apply the area formula with
max. - Return
answer. - Write the helper
removeAndUpdate. It pops every waiting bar taller thanheights[i], recordsias that bar's lower neighbour inarr, and finally pushesiitself.
The solution in Swift
class Solution {
func removeAndUpdate(_ heights: [Int], _ st: inout [Int], _ arr: inout [Int], _ i: Int) {
while !st.isEmpty && heights[i] < heights[st.last!] {
arr[st.last!] = i
st.removeLast()
}
st.append(i)
}
func largestRectangleArea(_ heights: [Int]) -> Int {
let n = heights.count
var left = [Int](repeating: -1, count: n)
var right = [Int](repeating: n, count: n)
var st = [Int]()
for i in 0..<n {
removeAndUpdate(heights, &st, &right, i)
}
st = [Int]()
for i in stride(from: n - 1, through: 0, by: -1) {
removeAndUpdate(heights, &st, &left, i)
}
var answer = 0
for i in 0..<n {
answer = max(answer, heights[i] * (right[i] - left[i] - 1))
}
return answer
}
}
Swift details worth a closer look:
inoutand&. Arrays are value types: passing one to a function normally gives the function its own copy. Marking a parameterinoutand writing&stat the call lets the helper change the caller's array. Without it the helper could not even assign tostorarr, because ordinary parameters are constants.- One helper, two directions. The helper never looks at the direction. It only compares
heights[i]with the bars waiting on the stack, so the same code fillsrightin one pass andleftin the other. st.last!.lastis an optional,nilfor an empty array. The force-unwrap is safe only because!st.isEmptyis checked first in the same condition, and&&stops evaluating when it is false.[Int](repeating:count:). Builds an array with one value repeated, which is howleftandrightstart with their defaults.stridewiththrough:.stride(from: n - 1, through: 0, by: -1)includes 0. Withto:it would stop before 0, andn - 1 ..< 0is not a valid range at all, because Swift traps when a range's upper bound is smaller than its lower bound. Forn = 0the stride is simply empty.- Strictly taller only. The loop pops bars that are strictly taller. Bars of equal height stay, so for
[2, 2, 2]the bounds areleft = [-1, -1, -1]andright = [3, 3, 3]: every bar reports the full width 3, and the area is 6.
Complexity
Time: O(n). In each pass every index is pushed once and popped at most once, so the while loop runs at most n times over the whole pass, not per bar. The two passes and the final loop each take linear time.
Space: O(n). left, right and the stack each hold up to n integers.
Edge cases and common mistakes
- One bar.
[5]givesleft = [-1]andright = [1], so the width is 1 and the area is 5. - A zero-height bar.
[0]gives 0. A zero also splits the histogram: no rectangle with positive height can cross it. - Equal heights. Popping with
<=instead of<makes equal bars cut each other off. For[2, 2, 2]every width would become 1 and the method would return 2 instead of 6. The lesson's version keeps equal bars inside the stretch. - Forgetting the default bounds. Bars left on the stack at the end have no lower bar on that side. Their entries must stay
-1andn, which gives them the full width to the edge. - Off-by-one in the width. The bars at
left[i]andright[i]are lower, so they are excluded. The width isright[i] - left[i] - 1, notright[i] - left[i]. - Forgetting
&orinout. Swift reports an error if aninoutargument is passed without&, and a helper withoutinoutcan't assign to its parameters.
Testing
[2, 1, 5, 6, 2, 3]gives10.[2, 4]gives4.[2, 2, 2]gives6.[3, 1, 3, 2, 2]gives6.[5]gives5, and[0]gives0.
