Skip to content

Commit b8e47c8

Browse files
committed
LeetCode 84. Largest Rectangle in Histogram
1 parent 85d2c86 commit b8e47c8

File tree

3 files changed

+219
-1
lines changed

3 files changed

+219
-1
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Solutions to LeetCode problems. The first column links to the problem in LeetCod
6868
| [78. Subsets][lc78] | 🟠 Medium | [![python](res/py.png)][lc78py] |
6969
| [79. Word Search][lc79] | 🟠 Medium | [![python](res/py.png)][lc79py] |
7070
| [83. Remove Duplicates from Sorted List][lc83] | 🟢 Easy | [![python](res/py.png)][lc83py] |
71+
| [84. Largest Rectangle in Histogram][lc84] | 🔴 Hard | [![python](res/py.png)][lc84py] |
7172
| [86. Partition List][lc86] | 🟠 Medium | [![python](res/py.png)][lc86py] |
7273
| [88. Merge Sorted Array][lc88] | 🟢 Easy | [![python](res/py.png)][lc88py] |
7374
| [90. Subsets II][lc90] | 🟠 Medium | [![python](res/py.png)][lc90py] |
@@ -423,6 +424,8 @@ Solutions to LeetCode problems. The first column links to the problem in LeetCod
423424
[lc79py]: leetcode/word-search.py
424425
[lc83]: https://leetcode.com/problems/remove-duplicates-from-sorted-list/
425426
[lc83py]: leetcode/remove-duplicates-from-sorted-list.py
427+
[lc84]: https://leetcode.com/problems/largest-rectangle-in-histogram/
428+
[lc84py]: leetcode/largest-rectangle-in-histogram.py
426429
[lc86]: https://leetcode.com/problems/partition-list/
427430
[lc86py]: leetcode/partition-list.py
428431
[lc88]: https://leetcode.com/problems/merge-sorted-array/
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# 84. Largest Rectangle in Histogram
2+
# 🔴 Hard
3+
#
4+
# https://leetcode.com/problems/largest-rectangle-in-histogram/
5+
#
6+
# Tags: Array - Stack - Monotonic Stack
7+
8+
import timeit
9+
from typing import List
10+
11+
# 10e3 calls.
12+
# » Naive 0.02904 seconds
13+
# » MonotonicStack 0.01783 seconds
14+
# » DivideAndConquer 0.08459 seconds
15+
16+
# Use a min monotonic stack where each entry is a tuple (height, index).
17+
# While correct, this implementation fails to pass the tests because it
18+
# computes the rectangle between all the heights in the stack and a
19+
# newly added height each time one is added. The next solution shows how
20+
# we can avoid doing this O(n^2) work.
21+
#
22+
# Time complexity: O(n^2) - In the worst case, we keep pushing new
23+
# heights into the stack without popping any, at O(n) per push.
24+
# Space complexity: O(n) - We store all heights in the stack.
25+
#
26+
# Fails with Time Limit Exceeded.
27+
class Naive:
28+
def largestRectangleArea(self, heights: List[int]) -> int:
29+
if len(heights) == 1:
30+
return heights[0]
31+
# Store the largest rectangle seen so far.
32+
res = 0
33+
# Initialize the stack with the height 0 just left of the
34+
# first index.
35+
stack = [(0, -1)]
36+
for idx, height in enumerate(heights):
37+
# Avoid adding this element as long as the next element has
38+
# the same height and it is not the last one. Pass LC 96.
39+
if idx < len(heights) - 1 and heights[idx + 1] == height:
40+
continue
41+
# Pop any heights greater than or equal to the current one.
42+
while stack and stack[-1][0] >= height:
43+
stack.pop()
44+
stack.append((height, idx))
45+
# Calculate the area of all rectangles that we can build
46+
# using the newly added height.
47+
for i in range(len(stack) - 1):
48+
# Area = base * h
49+
# The base for this height goes all the way from this
50+
# index to the current index.
51+
base = idx - stack[i][1]
52+
# The height is the height of the next lowest histogram
53+
# column.
54+
h = stack[i + 1][0]
55+
area = base * h
56+
if area > res:
57+
res = area
58+
59+
return res
60+
61+
62+
# Use a monotonic non-decreasing stack. Add boundary 0s to the start and
63+
# end of the heights array, to symbolize that rectangles cannot be
64+
# extended to include this positions. The left 0 boundary also
65+
# guarantees that the stack will never be empty, letting us avoid having
66+
# to check in each iteration of the inner loop. Iterate over the heights
67+
# array, when we see a height smaller than the top of the stack, we pop
68+
# it and calculate the rectangle that can be obtained between the
69+
# popped height and the first smaller height to its left. We keep the
70+
# largest area found to return as the result of the function.
71+
#
72+
# Time complexity: O(n) - We iterate once over the input array, each
73+
# height can be, at most, pushed into and popped from the stack once,
74+
# and we calculate an area every time we pop from the stack, that is a
75+
# maximum of n area calculations.
76+
# Space complexity: O(n) - The stack can grow to the same size as the
77+
# input.
78+
#
79+
# Runtime: 2061 ms, faster than 66.19%
80+
# Memory Usage: 27.1 MB, less than 99.30%
81+
class MonotonicStack:
82+
def largestRectangleArea(self, heights: List[int]) -> int:
83+
# Add two boundaries to the heights array. Initialize the stack
84+
# to contain the left 0 height to avoid having to check for
85+
# an empty stack at each iteration. Initialize the max at 0.
86+
heights, stack, res = [0] + heights + [0], [0], 0
87+
for i in range(1, len(heights)):
88+
# Shortcut to avoid computing each rectangle in a set of
89+
# bars of the same height, it will only compute the max.
90+
if heights[i] == heights[stack[-1]]:
91+
stack[-1] = i
92+
continue
93+
# Keep the stack non-decreasing by popping any greater
94+
# elements before appending.
95+
while heights[i] < heights[stack[-1]]:
96+
# Use any indexes popped from the stack to compute the
97+
# area of the rectangle that could be obtained between
98+
# that height and the next smaller height to its left.
99+
h = heights[stack.pop()]
100+
w = i - stack[-1] - 1
101+
res = max(res, h * w)
102+
stack.append(i)
103+
return res
104+
105+
106+
# Use a divide-and-conquer strategy, the max area of each section will
107+
# be the max area of the bars left of the middle, the bars right of the
108+
# middle, or the section that contains the middle bar.
109+
#
110+
# Time complexity: O(n*log(n)) - At each step the algorithm divides the
111+
# input into two halves and also calculates the result of using the
112+
# middle heights with O(n), the division can happen O(log(n)) times.
113+
# Space complexity: O(log(n)) - The call stack can grow to a height of
114+
# log(n), each call holds a reference to the input array and some
115+
# pointers, all of them constant space.
116+
#
117+
# Runtime: 9960 ms, faster than 5.1%
118+
# Memory Usage: 27.6 MB, less than 87.99%
119+
class DivideAndConquer:
120+
def maxCombinedArea(
121+
self, heights: List[int], l: int, mid: int, r: int
122+
) -> int:
123+
# Expand from the middle in O(1) to find the max area containing
124+
# the two central bars heights[mid] and heights[mid + 1]
125+
i, j, area = mid, mid + 1, 0
126+
h = min(heights[i], heights[j])
127+
while i >= l and j <= r:
128+
h = min(h, min(heights[i], heights[j]))
129+
area = max(area, (j - i + 1) * h)
130+
# If one side has reached the boundary, expand on the other
131+
# side only.
132+
if i == l:
133+
j += 1
134+
elif j == r:
135+
i -= 1
136+
# If both sides still have room to expand, do it on the side
137+
# that has a bigger height.
138+
else:
139+
if heights[i - 1] > heights[j + 1]:
140+
i -= 1
141+
else:
142+
j += 1
143+
return area
144+
145+
# Worker function that computes the max area between two points.
146+
def maxArea(self, heights: List[int], l: int, r: int) -> int:
147+
# The area of a single bar is its height.
148+
if l == r:
149+
return heights[l]
150+
mid = l + (r - l) // 2
151+
# Divide and conquer, the max area is either left, right or
152+
# contains mid.
153+
return max(
154+
self.maxArea(heights, l, mid), # Left.
155+
self.maxArea(heights, mid + 1, r), # Right.
156+
self.maxCombinedArea(heights, l, mid, r), # Overlapping mid.
157+
)
158+
159+
# Main function, exposes a public API expected by consumers.
160+
def largestRectangleArea(self, heights: List[int]) -> int:
161+
if not heights:
162+
return 0
163+
# For tests that contain only one value. LC 92 is 10e5 8793
164+
if len(set(heights)) == 1:
165+
return len(heights) * heights[0]
166+
# Call the worker function with the entire input array.
167+
return self.maxArea(heights, 0, len(heights) - 1)
168+
169+
170+
# DivideAndConquer solution inspired on the Java version here:
171+
# https://leetcode.com/problems/largest-rectangle-in-histogram/solutions/
172+
# 28910/simple-divide-and-conquer-ac-solution-without-segment-tree/
173+
174+
175+
def test():
176+
executors = [
177+
Naive,
178+
MonotonicStack,
179+
DivideAndConquer,
180+
]
181+
tests = [
182+
[[0], 0],
183+
[[0, 0], 0],
184+
[[2, 4], 4],
185+
[[2, 1, 5, 6, 2, 3], 10],
186+
[[0, 0, 0, 2, 4, 5, 0, 0, 0, 7, 0], 8],
187+
[
188+
[3, 1, 6, 4, 3, 6, 6, 2, 7, 1, 1, 4, 3, 2, 3, 4, 5, 6, 7, 6, 5, 4],
189+
28,
190+
],
191+
[
192+
[3, 2, 6, 4, 3, 6, 6, 2, 7, 2, 2, 4, 3, 2, 3, 4, 5, 6, 7, 6, 5, 4],
193+
44,
194+
],
195+
]
196+
for executor in executors:
197+
start = timeit.default_timer()
198+
for _ in range(1):
199+
for col, t in enumerate(tests):
200+
sol = executor()
201+
result = sol.largestRectangleArea(t[0])
202+
exp = t[1]
203+
assert result == exp, (
204+
f"\033[93m» {result} <> {exp}\033[91m for"
205+
+ f" test {col} using \033[1m{executor.__name__}"
206+
)
207+
stop = timeit.default_timer()
208+
used = str(round(stop - start, 5))
209+
cols = "{0:20}{1:10}{2:10}"
210+
res = cols.format(executor.__name__, used, "seconds")
211+
print(f"\033[92m» {res}\033[0m")
212+
213+
214+
test()

leetcode/lists/neetcode.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ From their website:
119119
|| | 🟠 Medium | [22. Generate Parentheses][lc22] | [![python](../../res/py.png)][lc22py] |
120120
|| | 🟠 Medium | [739. Daily Temperatures][lc739] | [![python](../../res/py.png)][lc739py] |
121121
|| | 🟠 Medium | [853. Car Fleet][lc853] | [![python](../../res/py.png)][lc853py] |
122-
| | | 🔴 Hard | [84. Largest Rectangle in Histogram][lc84] | |
122+
| | | 🔴 Hard | [84. Largest Rectangle in Histogram][lc84] | [![python](../../res/py.png)][lc84py] |
123123

124124
[lc20]: https://leetcode.com/problems/valid-parentheses/
125125
[lc20py]: ../valid-parentheses.py
@@ -134,6 +134,7 @@ From their website:
134134
[lc853]: https://leetcode.com/problems/car-fleet/
135135
[lc853py]: ../car-fleet.py
136136
[lc84]: https://leetcode.com/problems/largest-rectangle-in-histogram/
137+
[lc84py]: ../largest-rectangle-in-histogram.py
137138

138139
## Binary Search
139140

0 commit comments

Comments
 (0)