2025, Dec 06 06:02
Largest Rectangle in Histogram: решение монотонным стеком за O(n)
Разбираем задачу Largest Rectangle in Histogram: как найти максимальный прямоугольник в гистограмме с помощью монотонного стека за O(n). Объяснение и код C++.
Задача «Largest Rectangle in Histogram» из тех случаев, когда брутфорс кажется заманчивым, но прорыв происходит, когда переосмысливаешь цель. Секрет не в переборе всех возможных диапазонов и не в построении дерева решений. Вместо этого смотрим на каждый столбец и задаём один вопрос: если высота этого столбца — ограничивающая высота прямоугольника, насколько широко он может растянуться?
Задача в одном предложении
Даны высоты столбцов гистограммы. Нужно найти площадь наибольшего прямоугольника, полностью помещающегося внутри гистограммы. Пример: для [2,1,5,6,2,3] ответ равен 10 (его дают столбцы [5,6] при ширине 2 и высоте 5).
Наивный подход, который не укладывается по времени
Ниже показан рекурсивный перебор, который накапливает диапазоны и хранит текущий минимум по высоте. Он пробует начать новый диапазон или продолжить текущий и на каждом шаге обновляет лучшую площадь. Работает корректно, но в худшем случае имеет экспоненциальную сложность и не масштабируется.
class HistMax:
def rect_max_area(self, bars: List[int]) -> int:
best = 0
def explore(pos, cur_min, span, arr):
nonlocal best
area_here = cur_min * span
best = max(best, area_here)
if pos < len(arr):
if arr[pos] > cur_min:
explore(pos + 1, arr[pos], 1, arr)
explore(pos + 1, min(arr[pos], cur_min), span + 1, arr)
explore(0, float('inf'), 0, bars)
return best
Ключевая идея
Цель проста: найти индекс, чья высота задаёт максимальный прямоугольник. Наибольший прямоугольник обязательно использует одну из существующих высот как ограничивающую. Для фиксированного индекса i с высотой heights[i] прямоугольник высоты heights[i] может расширяться влево и вправо, пока все столбцы не ниже heights[i]. Это естественным образом задаёт две границы: первый строго более низкий столбец слева и первый строго более низкий справа. Всё между этими двумя более низкими столбцами поддерживает прямоугольник высоты heights[i].
Отсюда важная формулировка: «Для каждого индекса i сохранить индексы первого столбца слева и справа, соответственно, который ниже, чем height[i]». Эти две границы определяют максимальную ширину, на которой heights[i] может выступать высотой прямоугольника. Зная такие границы для каждого i, остаётся посчитать площади всех кандидатов и взять максимум. Найти границы эффективно помогает монотонный стек.
Эффективное решение с монотонным стеком
Ниже — реализация со стеком индексов, поддерживающим неубывающую последовательность высот. Когда появляется более низкий столбец (или мы доходим до «сторожа» в конце), становится ясно, что у извлечённого индекса найден правый предел, а новый верх стека раскрывает левую границу. Ширина выводится из этих двух. Алгоритм работает за линейное время относительно числа столбцов.
class Histogram {
public:
int maxRectArea(vector<int>& bars) {
int result = 0;
stack<int> idxs;
for (int p = 0; p <= bars.size(); ++p) {
while (!idxs.empty() &&
(p == bars.size() || bars[idxs.top()] > bars[p])) {
const int h = bars[idxs.top()];
idxs.pop();
const int w = idxs.empty() ? p : p - idxs.top() - 1;
result = max(result, h * w);
}
idxs.push(p);
}
return result;
}
};
Почему границы так важны
Для любого индекса i левые и правые границы ровно определяют, насколько можно расширяться, сохраняя heights[i] управляющей высотой. За пределами этих границ встречается строго более низкий столбец, и допустимая высота сразу падает ниже heights[i]. Поэтому максимальный «собственный» прямоугольник индекса i полностью определяется первыми более низкими столбцами по обе стороны. Достаточно записать или найти эти границы для всех i — и задача решена.
Почему это полезно
Этот шаблон встречается во многих задачах по массивам, где требуется «первый меньший/больший слева/справа». Понимание приёма помогает распознать, когда монотонный стек может свернуть брутфорс до одного линейного прохода. На практике это разница между тайм-аутами и оптимальным решением.
Выводы
Сформулируйте цель так: выбрать индекс, чья высота даст максимальный прямоугольник. Для этого индекса определите, как далеко влево и вправо можно расширяться до первых строго более низких столбцов. Используйте монотонный стек, чтобы быстро найти эти границы и на лету посчитать лучшую площадь.