Monotonic stack

Monotonic stack — это стек, в котором мы специально поддерживаем
монотонный порядок элементов: либо возрастающий, либо убывающий.

То есть это не просто стек «последний вошёл — первый вышел», а стек с правилом:

пока новый элемент ломает порядок —
    выкидываем элементы сверху стека
потом кладём новый элемент

Главная идея:

стек хранит только тех кандидатов, которые ещё могут пригодиться

А те, кто уже точно проиграл новому элементу, удаляются.

Есть массив:

[2, 1, 5, 3, 4]

Допустим, мы хотим найти для каждого числа следующее большее справа.

То есть:

2 -> 5
1 -> 5
5 -> нет
3 -> 4
4 -> нет

Для этого удобно использовать monotonic decreasing stack — стек, где
элементы ждут следующее большее число.

Идём слева направо.

Видим 2:

stack = [2]

Пока ничего не знаем.

Видим 1.

1 не больше 2, значит не может закрыть 2.

stack = [2, 1]

Видим 5.

5 больше 1, значит для 1 найдено следующее большее:

1 -> 5
stack = [2]

5 больше 2, значит для 2 тоже найдено следующее большее:

2 -> 5
stack = []

Кладём 5:

stack = [5]

Видим 3.

3 не больше 5.

stack = [5, 3]

Видим 4.

4 больше 3:

3 -> 4
stack = [5]

Но 4 не больше 5, значит 5 остаётся ждать.

stack = [5, 4]

В конце те, кто остался в стеке, не нашли большего справа:

5 -> нет
4 -> нет

Базовый шаблон

Для задач типа next greater element:

def next_greater(nums):
    result = [-1] * len(nums)
    stack = []  # здесь будут индексы

    for i, x in enumerate(nums):
        while stack and nums[stack[-1]] < x:
            j = stack.pop()
            result[j] = x

        stack.append(i)

    return result

Для:

nums = [2, 1, 5, 3, 4]

результат:

[5, 5, -1, 4, -1]

Почему в стеке обычно хранят индексы

Можно хранить значения:

stack = [2, 1]

Но чаще хранят индексы:

stack = [0, 1]

Потому что индекс даёт доступ сразу к двум вещам:

nums[index]      # само значение
index            # позиция элемента

Это важно, например, в задаче Daily Temperatures, где нужно не просто найти
следующую большую температуру, а посчитать расстояние до неё.

Пример: Daily Temperatures

Задача:

temperatures = [73, 74, 75, 71, 69, 72, 76, 73]

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

Ответ:

[1, 1, 4, 2, 1, 1, 0, 0]

Код:

def daily_temperatures(temperatures):
    result = [0] * len(temperatures)
    stack = []  # индексы дней, для которых ещё не нашли более тёплый день

    for i, temp in enumerate(temperatures):
        while stack and temperatures[stack[-1]] < temp:
            prev_i = stack.pop()
            result[prev_i] = i - prev_i

        stack.append(i)

    return result

Здесь стек хранит дни, температура которых ещё ждёт более тёплого дня.

Почему это O(n), хотя внутри есть while

На первый взгляд кажется:

for + while = O(n^2)

Но нет.

Каждый элемент:

1 раз добавляется в стек
1 раз удаляется из стека

То есть элемент не может бесконечно pop-аться. Его положили, потом однажды
выкинули — всё.

Поэтому общее количество операций push и pop за весь алгоритм:

O(n)

Это называется amortized O(n).

Локально один шаг может выкинуть много элементов, но суммарно за весь проход
каждый элемент выкидывается максимум один раз.

Increasing stack и decreasing stack

Есть две основные формы.

Monotonic increasing stack

Стек поддерживает возрастающий порядок.

Например:

[1, 3, 5, 8]

Когда приходит новый элемент меньше верхнего, мы удаляем верхние элементы.

Шаблон:

while stack and stack[-1] > x:
    stack.pop()

stack.append(x)

Такой стек часто помогает искать:

previous smaller
next smaller
минимальные элементы
лексикографически минимальную последовательность

Monotonic decreasing stack

Стек поддерживает убывающий порядок.

Например:

[8, 5, 3, 1]

Когда приходит новый элемент больше верхнего, мы удаляем верхние элементы.

Шаблон:

while stack and stack[-1] < x:
    stack.pop()

stack.append(x)

Такой стек часто помогает искать:

previous greater
next greater
следующий больший элемент
более тёплый день

Главное различение

seen set:

элемент -> уже был или нет

frequency map:

элемент -> сколько раз встретился

grouping:

ключ -> список элементов

monotonic stack:

держим кандидатов в порядке,
а тех, кто больше не может быть ответом, выкидываем

То есть это не структура данных сама по себе, а паттерн управления стеком.

Интуитивная формула

Monotonic stack отвечает на вопрос:

какие элементы ещё ждут своего закрывающего элемента справа?

Например:

число ждёт следующее большее число
температура ждёт более тёплый день
здание ждёт более высокое здание
цена ждёт падение/рост

Когда появляется новый элемент, он может закрыть несколько старых элементов
сразу.

Где часто встречается

Классические задачи:

Next Greater Element
Next Smaller Element
Daily Temperatures
Largest Rectangle in Histogram
Trapping Rain Water
Remove K Digits
Stock Span Problem
Sum of Subarray Minimums

Особенно характерный признак задачи:

найти ближайший следующий больший/меньший элемент слева или справа

Если видишь формулировку:

next greater
next smaller
previous greater
previous smaller
nearest greater
nearest smaller

почти всегда где-то рядом лежит monotonic stack.

Прокрутить вверх