Largest Rectangle in Histogram
Гистограмма — это ряд столбцов, стоящих бок о бок без промежутков, каждый шириной в одну единицу: heights[i] — это высота столбца i. Прямоугольник внутри неё охватывает последовательность соседних столбцов и не может быть выше самого низкого столбца в этой последовательности.
Верните наибольшую площадь, которую может иметь такой прямоугольник.
Функция
- heightsinteger-array
- высота каждого столбца слева направо
- Возвращаетinteger
- площадь наибольшего прямоугольника, который помещается в гистограмме
Ограничения
1 ≤ heights.length ≤ 2 × 1040 ≤ heights[i] ≤ 105- Каждый столбец имеет ширину в одну единицу, поэтому прямоугольник над столбцами от
iдоjимеет ширинуj-i+1единиц.
Примеры
- Ввод
- heights = [2, 5, 6, 3, 4, 1]
- Вывод
- 12
- Пояснение
- Все четыре столбца высотой 5, 6, 3 и 4 имеют высоту не менее 3, поэтому прямоугольник высотой 3 охватывает их: 3 × 4 = 12. Два самых высоких столбца, 5 и 6, дают только 5 × 2 = 10.
- Ввод
- heights = [1, 8, 1, 1]
- Вывод
- 8
- Пояснение
- Полоса из одних 8 даёт 8 × 1 = 8. Любой более широкий прямоугольник включает полосу из 1, поэтому его размер не превышает 1 × 4 = 4.
- Ввод
- heights = [3, 3, 3, 3]
- Вывод
- 12
- Пояснение
- Все четыре столбца имеют высоту 3, поэтому вся гистограмма представляет собой один прямоугольник: 3 × 4 = 12.
+17 скрытых тестов при отправке
Дополнительный вопрос
Предположим, что у каждого столбца своя ширина, заданная во втором массиве. Что изменится в решении с использованием стека за один проход?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Самый большой прямоугольник касается верхней границы хотя бы одного столбца под ним: если бы он этого не делал, его можно было бы сделать выше. Поэтому попробуй каждый столбец в качестве столбца, задающего высоту. Какой ширины может быть прямоугольник именно такой высоты?
Прямоугольник высотой с планку
iпростирается влево и вправо, пока не встретит с каждой стороны планку строго меньшей высоты. Если известна ближайшая планка меньшей высоты с каждой стороны для каждой планки, каждая планка задаёт один возможный вариант площади, а всего таких вариантов толькоn.Храни стек индексов, высоты которых возрастают снизу вверх. Когда появляется столбец, высота которого не больше высоты верхнего, верхний столбец уже не может распространяться дальше вправо: извлеки его из стека — его прямоугольник покрывает столбцы строго между новым верхним элементом стека и текущим столбцом. Столбец высоты 0 после конца извлекает из стека всё, что осталось.
Решение
Прямоугольник может начинаться и заканчиваться у любого столбца, а его высота зависит от самого низкого столбца, который он охватывает, поэтому перебор всех последовательных отрезков столбцов требует примерно n²/2 шагов. Решение — перевернуть задачу: высота наилучшего прямоугольника в точности равна высоте одного из столбцов, поэтому каждому столбцу нужно знать лишь, насколько далеко он может простираться, прежде чем его остановит более низкий столбец. Монотонный стек находит эти границы для каждого столбца: сначала за два прохода, а затем за один.
Попробуй каждый запуск с текущим минимумом
Верно, но не успевает на самых больших тестах
Идея
Прямоугольник покрывает последовательность соседних столбиков от start до end, а его высота ограничена высотой самого низкого столбика в этой последовательности. Поэтому попробуй все последовательности. Зафиксируй start, затем увеличивай end на один столбик за раз и отслеживай минимальную высоту, встреченную на данный момент. Площадь наибольшего прямоугольника для этой последовательности равна lowest × (end-start+1).
В [2, 5, 6, 3, 4, 1] начни с 5. Для последовательностей получаются площади 5 × 1 = 5, затем 5 × 2 = 10 с 6, потом 3 × 3 = 9, когда добавляется 3, 3 × 4 = 12 с 4 и 1 × 5 = 5 с 1. Ответ — 12. Обновление lowest по мере роста последовательности позволяет выполнять каждый шаг за O(1), поэтому тебе не нужно заново просматривать последовательность, чтобы найти её минимум.
Алгоритм корректен, потому что каждый прямоугольник располагается над некоторой последовательностью, а для фиксированной последовательности самый высокий подходящий прямоугольник имеет высоту, в точности равную высоте самого низкого столбика. Алгоритм медленный, потому что существует n(n+1)/2 последовательностей: около 2 × 10^8 для 2 × 10^4 столбиков, и это количество никак не зависит от их высоты. Многие из этих последовательностей обрываются из-за низкого столбика задолго до конца, но алгоритм полного перебора всё равно продолжает их расширять.
Алгоритм
- Установи
bestв 0. - Для каждого
startустановиlowestравнымheights[start]. - Для каждого
endотstartдо последнего столбца, уменьши значениеlowestдоheights[end], если этот столбец ниже. - Обнови
bestзначениемlowest × (end-start+1). - Верни
best.
def largestRectangleArea(heights):
n = len(heights)
best = 0
for start in range(n):
lowest = heights[start]
for end in range(start, n):
# The rectangle over start..end is as tall as the lowest bar in it.
lowest = min(lowest, heights[end])
best = max(best, lowest * (end - start + 1))
return bestБлижайший более короткий столбец с каждой стороны
Идея
Изменим подход к поиску. В наилучшем прямоугольнике хотя бы один столбец под ним имеет высоту, в точности равную высоте прямоугольника; иначе прямоугольник можно было бы поднять. Значит, ответ — максимум среди всех столбцов i площади прямоугольника высотой ровно heights[i], который может быть как можно шире. Он простирается, пока с каждой стороны не встретит столбец строго меньшей высоты. Назовём их индексы left[i] и right[i], а если таких столбцов нет, используем -1 и n. Прямоугольник покрывает столбцы строго между ними: ширина right[i]-left[i]-1. Так мы получаем n кандидатов вместо n²/2.
Чтобы найти left[i] для каждого столбца, пройдём слева направо со стеком индексов, высоты столбцов которых строго возрастают от дна к вершине. Когда встречается столбец i, удаляем из стека все индексы столбцов, высота которых не меньше heights[i]. Эти столбцы уже никогда не смогут быть ближайшими столбцами меньшей высоты ни для i, ни для любого столбца после него, потому что i ближе и не выше. Оставшийся на вершине индекс — ближайший слева столбец меньшей высоты. Затем добавляем i в стек. Такой же проход справа налево даёт right[i].
Для [2, 5, 6, 3, 4, 1] проходы дают left = [-1, 0, 1, 0, 3, -1] и right = [5, 3, 3, 5, 5, 6]. Столбец высоты 3 с индексом 3 ограничен столбцом высоты 2 с индексом 0 и столбцом высоты 1 с индексом 5, поэтому его прямоугольник имеет площадь 3 × (5-0-1) = 12. Столбец высоты 6 зажат соседями, и площадь его прямоугольника равна только 6 × 1.
Каждый индекс добавляется в стек один раз и удаляется не более одного раза за проход, поэтому оба прохода занимают O(n), хотя один столбец может удалить из стека много элементов. Цена этого решения — два дополнительных массива.
Алгоритм
- Пройдите слева направо с пустым стеком. Для каждого
iизвлекайте элементы, пока столбец на вершине не станет не нижеheights[i]; присвойтеleft[i]значение на вершине или -1, если стек пуст; добавьтеiв стек. - Аналогично пройдите справа налево, чтобы заполнить
right[i], используяnдля пустого стека. - Для каждого
iвычислитеheights[i] × (right[i]-left[i]-1). - Верните наибольшую из этих площадей.
def largestRectangleArea(heights):
n = len(heights)
left = [-1] * n # index of the nearest shorter bar on the left, or -1
right = [n] * n # index of the nearest shorter bar on the right, or n
stack = [] # indices whose heights rise strictly from bottom to top
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
left[i] = stack[-1]
stack.append(i)
stack = []
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
right[i] = stack[-1]
stack.append(i)
best = 0
for i in range(n):
# Bar i is the lowest bar of everything strictly between left[i] and right[i].
best = max(best, heights[i] * (right[i] - left[i] - 1))
return bestОдин проход с монотонным стеком
Идея
При проходе слева направо уже видна каждая правая граница, но она отбрасывается. Когда столбец i удаляет столбец t, heights[i] не выше, чем heights[t], поэтому i — место, где прямоугольник столбца t заканчивается справа. А индекс, оставшийся под t в стеке, указывает, где он заканчивается слева. Поэтому измеряй прямоугольник в момент удаления: heights[t] × (i - below - 1), где below — новая вершина стека или -1, если стек теперь пуст.
Инвариант: высоты в стеке строго возрастают снизу вверх, а индекс под каждой записью — ближайший слева столбец, высота которого меньше её высоты. Каждый столбец между ними был удалён по пути — либо самой записью, либо столбцом, который эта запись удалила позже, — поэтому ни один из них не ниже данной записи. Столбцы, которые так и не будут удалены, тянутся до самого конца, поэтому после обработки последнего столбца обработай ещё один столбец высотой 0. Он ниже всех остальных и опустошает стек.
Рассмотрим [2, 5, 6, 3, 4, 1]. Добавь в стек 2, 5 и 6: в стеке находятся индексы [0, 1, 2]. Число 3 с индексом 3 удаляет 6 (площадь 6 × (3-1-1) = 6) и 5 (площадь 5 × (3-0-1) = 10), затем останавливается на 2 и добавляется в стек. Добавь 4. Число 1 с индексом 5 удаляет 4 (площадь 4), затем 3, прямоугольник которого тянется от индекса 1 до 4: 3 × (5-0-1) = 12. Оно удаляет и 2 (2 × 5 = 10; стек пуст, поэтому ширина равна 5). Завершающий 0 удаляет 1 (1 × 6 = 6). Максимальная площадь равна 12.
Удаление при условии >= означает, что равный по высоте столбец может преждевременно остановить другой столбец. Это безопасно: равный столбец занимает его место в стеке, наследует ту же левую границу, а при последующем удалении его прямоугольник охватывает весь ряд. Для [3, 3, 3, 3] первые три столбца высотой 3 задают ширину 1, 2 и 3, а последний удаляется завершающим 0 с шириной 4, что даёт 12.
Алгоритм
- Начните с пустого стека индексов и
best = 0. - Для
iот 0 доnзадайте текущую высоту равнойheights[i]или 0, еслиi = n. - Пока столбец на вершине стека не ниже текущей высоты, извлекайте его как
t; ширина равнаi - below - 1, гдеbelow— новая вершина стека или -1; обновитеbestзначениемheights[t] × width. - Добавьте
iв стек. - Верните
best.
def largestRectangleArea(heights):
n = len(heights)
stack = [] # indices whose heights rise strictly from bottom to top
best = 0
for i in range(n + 1):
current = heights[i] if i < n else 0 # a bar of 0 past the end empties the stack
while stack and heights[stack[-1]] >= current:
# Bar i stops the popped bar on the right; the bar below it stops it on the left.
height = heights[stack.pop()]
left = stack[-1] if stack else -1
best = max(best, height * (i - left - 1))
stack.append(i)
return best
Ловушки и крайние случаи
Цикл со стеком короткий, и почти все ошибки связаны с шириной или с оставшимися в конце столбцами.
- Забыть о столбцах, которые всё ещё находятся в стеке. В возрастающей гистограмме, например
[1, 2, 3, 4, 5], внутри цикла ни один столбец не извлекается из стека, и без завершающего столбца высотой 0 вы вернёте 0 вместо 9. - Вычислять ширину, используя индекс извлечённого столбца. Его прямоугольник начинается сразу после столбца под ним в стеке, а не с него самого: в
[2, 5, 6, 3, 4, 1]столбец высотой 3 с индексом 3 охватывает индексы от 1 до 4. Использованиеi - tдаёт 2 вместо 4. - Использовать неверную ширину, если после извлечения стек пуст. Извлечённый столбец — самый низкий на данный момент, поэтому его прямоугольник тянется до индекса 0, а ширина равна
i. В[2, 1, 2]столбец высотой 1 охватывает все три столбца, и площадь равна 3. - Останавливаться на равных столбцах с обеих сторон в двухпроходном варианте. Тогда в
[3, 3, 3, 3]ширина для каждого столбца будет равна 1, и вы вернёте 3 вместо 12. Извлекайте из стека при условии>=, чтобы границами служили строго более низкие столбцы. - Полагать, что наибольший столбец или самый широкий диапазон даст наилучший результат. В
[2, 5, 6, 3, 4, 1]ответ не дают ни высота 6, ни вся ширина из 6 столбцов; его даёт средняя высота на средней ширине. - Переполнение. Здесь площадь может достигать
10^5 × 2 × 10^4 = 2 × 10^9, что всё ещё помещается в знаковое 32-битное целое число; при больших ограничениях выполняйте умножение в 64-битном формате.
Частые вопросы4
Какова временная сложность алгоритма поиска наибольшего прямоугольника в гистограмме?
Решение с монотонным стеком работает за время O(n) и требует O(n) дополнительной памяти. Каждый индекс добавляется в стек один раз и извлекается из него один раз, а при каждом извлечении выполняется постоянное количество операций. Перебор каждого участка столбцов занимает время O(n²) — около 2 × 10^8 шагов для 2 × 10^4 столбцов.
Почему измеряется прямоугольник столбца, когда он извлекается?
Плитка удаляется первой плиткой справа от неё, которая не выше, поэтому там заканчивается её прямоугольник справа. Индекс под ней в стеке указывает на ближайшую более низкую плитку слева, поэтому там прямоугольник заканчивается слева. В момент удаления известны оба конца, и площадь равна height × (i - below - 1).
Можно ли решить задачу «Наибольший прямоугольник в гистограмме» методом «разделяй и властвуй»?
Да. Самая низкая полоска во всём диапазоне либо находится под наилучшим прямоугольником, площадь которого тогда равна lowest × width, либо делит диапазон на левую и правую части, которые нужно решить по отдельности. При линейном поиске минимума сложность составляет O(n log n) для случайных входных данных, но O(n²) для отсортированных; дерево отрезков для поиска минимума на диапазоне всегда обеспечивает сложность O(n log n). Стек проще и быстрее.
Как задача о наибольшем прямоугольнике в гистограмме используется для нахождения максимального прямоугольника в сетке из 0 и 1?
Просматривай сетку строка за строкой и храни для каждого столбца количество единиц подряд, заканчивающихся на текущей строке; ноль сбрасывает этот счётчик. Счётчики каждой строки образуют гистограмму, а наибольший прямоугольник из единиц, заканчивающийся на этой строке, — это наибольший прямоугольник в этой гистограмме. Однократный проход стеком для каждой строки решает задачу для всей сетки за время O(rows × cols).
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def largestRectangleArea(heights):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
heights = [2, 5, 6, 3, 4, 1]
Ожидается
12