MidПрактика11 min

Монотонный стек

Монотонно возрастающий/убывающий стек: next greater element, temperatures, stock span

Монотонный стек (Monotonic Stack)

Идея

Монотонный стек — это стек, в котором элементы поддерживают строгий порядок (возрастающий или убывающий). При добавлении нового элемента мы выталкиваем все элементы, нарушающие порядок.

Монотонно убывающий стек (сверху вниз — убывание):

Добавляем 4:          Добавляем 2:         Добавляем 5:
  |   |                 | 2 |                |   |
  | 4 |                 | 4 |                | 5 |
  | 7 |                 | 7 |                | 7 |
  +---+                 +---+                +---+
                                          (4 и 2 вытолкнуты)

Зачем нужен

Монотонный стек позволяет для каждого элемента найти ближайший больший/меньший элемент слева или справа за O(n).

Без монотонного стека: O(n²) — для каждого элемента ищем вправо/влево. С монотонным стеком: O(n) — каждый элемент добавляется и удаляется максимум один раз.

Задача 1: Next Greater Element

Условие: для каждого элемента найти первый больший элемент справа.

<?php
declare(strict_types=1);

/**
 * @param int[] $nums
 * @return int[]
 */
function nextGreaterElement(array $nums): array
{
    $n = count($nums);
    $result = array_fill(0, $n, -1);
    $stack = []; // Stores indices

    for ($i = 0; $i < $n; $i++) {
        // While current element is greater than the element at stack top
        while (!empty($stack) && $nums[$i] > $nums[end($stack)]) {
            $idx = array_pop($stack);
            $result[$idx] = $nums[$i];
        }
        $stack[] = $i;
    }

    return $result;
}

// nums = [2, 1, 2, 4, 3]
//
// i=0, num=2: stack=[], push 0.        stack=[0]
// i=1, num=1: 1 < nums[0]=2, push 1.   stack=[0,1]
// i=2, num=2: 2 > nums[1]=1, pop 1 -> result[1]=2
//             2 = nums[0]=2 (not >), push 2. stack=[0,2]
// i=3, num=4: 4 > nums[2]=2, pop 2 -> result[2]=4
//             4 > nums[0]=2, pop 0 -> result[0]=4
//             push 3.                   stack=[3]
// i=4, num=3: 3 < nums[3]=4, push 4.   stack=[3,4]
//
// result = [4, 2, 4, -1, -1]
## Задача 2: Daily Temperatures

Условие: для каждого дня найти, через сколько дней будет теплее.

<?php
declare(strict_types=1);

/**
 * @param int[] $temps
 * @return int[]
 */
function dailyTemperatures(array $temps): array
{
    $n = count($temps);
    $result = array_fill(0, $n, 0);
    $stack = []; // Day indices

    for ($i = 0; $i < $n; $i++) {
        while (!empty($stack) && $temps[$i] > $temps[end($stack)]) {
            $prevDay = array_pop($stack);
            $result[$prevDay] = $i - $prevDay;
        }
        $stack[] = $i;
    }

    return $result;
}

// temps = [73, 74, 75, 71, 69, 72, 76, 73]
// result = [1, 1, 4, 2, 1, 1, 0, 0]
//
// For 73 (day 0): next day 74 -> 1
// For 75 (day 2): after 4 days 76 -> 4
// For 76 (day 6): no warmer day -> 0
Визуализация:
Температуры: [73, 74, 75, 71, 69, 72, 76, 73]
Индексы:       0   1   2   3   4   5   6   7

Стек (индексы):

i=0: 73. stack=[]         -> push. stack=[0]
i=1: 74 > 73.             -> pop 0, result[0]=1-0=1. push. stack=[1]
i=2: 75 > 74.             -> pop 1, result[1]=2-1=1. push. stack=[2]
i=3: 71 < 75.             -> push. stack=[2,3]
i=4: 69 < 71.             -> push. stack=[2,3,4]
i=5: 72 > 69, pop 4       -> result[4]=5-4=1
     72 > 71, pop 3       -> result[3]=5-3=2
     72 < 75.             -> push. stack=[2,5]
i=6: 76 > 72, pop 5       -> result[5]=6-5=1
     76 > 75, pop 2       -> result[2]=6-2=4
                          -> push. stack=[6]
i=7: 73 < 76.             -> push. stack=[6,7]

result = [1, 1, 4, 2, 1, 1, 0, 0]

Задача 3: Largest Rectangle in Histogram

Условие: массив высот столбцов гистограммы. Найти площадь наибольшего прямоугольника.

Высоты: [2, 1, 5, 6, 2, 3]

     +--+
     |  |
  +--+  |
  |  |  |
  |  |  |  +--+
+--+ |  +--+  |
|  | |  |  |  |
|  | |  |  |  |
+--+--+--+--+--+--+
  2  1  5  6  2  3

Наибольший прямоугольник: 5*2 = 10 (столбцы 5 и 6)
<?php
declare(strict_types=1);

/**
 * @param int[] $heights
 */
function largestRectangleHistogram(array $heights): int
{
    $stack = []; // Indices
    $maxArea = 0;
    $heights[] = 0; // Append 0 to flush remaining elements

    foreach ($heights as $i => $h) {
        while (!empty($stack) && $heights[end($stack)] > $h) {
            $height = $heights[array_pop($stack)];
            $width = empty($stack) ? $i : $i - end($stack) - 1;
            $maxArea = max($maxArea, $height * $width);
        }
        $stack[] = $i;
    }

    return $maxArea;
}

// heights = [2, 1, 5, 6, 2, 3]
// Answer: 10
## Задача 4: Stock Span

Условие: для каждого дня найти количество подряд идущих дней до него (включая текущий), когда цена была <= текущей.

<?php
declare(strict_types=1);

/**
 * @param int[] $prices
 * @return int[]
 */
function stockSpan(array $prices): array
{
    $n = count($prices);
    $spans = array_fill(0, $n, 0);
    $stack = []; // Indices

    for ($i = 0; $i < $n; $i++) {
        while (!empty($stack) && $prices[end($stack)] <= $prices[$i]) {
            array_pop($stack);
        }

        $spans[$i] = empty($stack) ? $i + 1 : $i - end($stack);
        $stack[] = $i;
    }

    return $spans;
}

// prices = [100, 80, 60, 70, 60, 75, 85]
// spans  = [1,   1,  1,  2,  1,  4,  6]
## Типы монотонных стеков
Тип Порядок в стеке Находит
Монотонно убывающий Вершина < дно Next Greater Element
Монотонно возрастающий Вершина > дно Next Smaller Element

Next Smaller Element

<?php
declare(strict_types=1);

/**
 * @param int[] $nums
 * @return int[]
 */
function nextSmallerElement(array $nums): array
{
    $n = count($nums);
    $result = array_fill(0, $n, -1);
    $stack = [];

    for ($i = 0; $i < $n; $i++) {
        while (!empty($stack) && $nums[$i] < $nums[end($stack)]) {
            $idx = array_pop($stack);
            $result[$idx] = $nums[$i];
        }
        $stack[] = $i;
    }

    return $result;
}

// nums = [4, 8, 5, 2, 25]
// result = [2, 5, 2, -1, -1]
### Previous Greater Element (обход справа налево)
<?php
declare(strict_types=1);

/**
 * @param int[] $nums
 * @return int[]
 */
function previousGreaterElement(array $nums): array
{
    $n = count($nums);
    $result = array_fill(0, $n, -1);
    $stack = [];

    for ($i = $n - 1; $i >= 0; $i--) {
        while (!empty($stack) && $nums[end($stack)] <= $nums[$i]) {
            $idx = array_pop($stack);
            $result[$idx] = $nums[$i];
        }
        $stack[] = $i;
    }

    return $result;
}
## Шаблон
<?php
declare(strict_types=1);

/**
 * Generic monotonic stack template.
 *
 * @param int[] $nums
 * @return int[]
 */
function monotonicStackTemplate(
    array $nums,
    bool $findGreater = true,
    bool $findRight = true,
): array {
    $n = count($nums);
    $result = array_fill(0, $n, -1);
    $stack = [];

    $compare = $findGreater
        ? fn(int $a, int $b): bool => $a > $b
        : fn(int $a, int $b): bool => $a < $b;

    if ($findRight) {
        for ($i = 0; $i < $n; $i++) {
            while (!empty($stack) && $compare($nums[$i], $nums[end($stack)])) {
                $idx = array_pop($stack);
                $result[$idx] = $nums[$i]; // or $i for index
            }
            $stack[] = $i;
        }
    } else {
        for ($i = $n - 1; $i >= 0; $i--) {
            while (!empty($stack) && $compare($nums[$i], $nums[end($stack)])) {
                $idx = array_pop($stack);
                $result[$idx] = $nums[$i];
            }
            $stack[] = $i;
        }
    }

    return $result;
}
> **Запомни:** Монотонный стек решает задачи типа «для каждого элемента найти ближайший больший/меньший» за O(n). Каждый элемент входит в стек максимум один раз и выходит максимум один раз, поэтому суммарно O(n). Три ключевые задачи: Next Greater Element, Daily Temperatures, Largest Rectangle in Histogram.

Итоги

  1. Монотонный стек поддерживает порядок элементов (возр. или убыв.)
  2. Решает «next greater/smaller» за O(n) вместо O(n²)
  3. Каждый элемент push/pop максимум один раз = O(n) итого
  4. Largest Rectangle in Histogram — классика hard-задач
  5. Храни индексы в стеке (не значения) — так удобнее считать расстояния

Проверь себя

В задаче Largest Rectangle in Histogram для `[2, 1, 5, 6, 2, 3]` к массиву высот добавляется 0 в конец. Зачем?

Почему монотонный стек работает за O(n), хотя внутри цикла есть вложенный while?

Какой тип монотонного стека нужен для нахождения Next Smaller Element?

В задаче Daily Temperatures для массива `[73, 74, 75, 71, 69]` — какой ответ?

Какой результат nextGreaterElement для массива `[3, 1, 2, 4]`?