Алгоритмы и структуры данных

Monotonic Stack and Deque

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

актуальноТекущий рабочий стандарт

Направления: Основы · Алгоритмы и структуры данных

Приём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в — за один проход.

Ключевые тезисы

  • Пришедший элемент закрывает вопрос сразу для всех меньших: они уже никогда не станут ответом.
  • Каждый индекс кладётся и снимается один раз, поэтому вложенный while внутри цикла линеен, а не квадратичен.
  • Четыре комбинации «больший или меньший, справа или слева» покрывают почти всё семейство задач.

Какую задачу решает

«Для каждого элемента найдите ближайший больший справа» — формулировка, за которой прячется целое семейство задач: сколько дней ждать потепления, какой прямоугольник максимален в гистограмме, сколько воды удержит рельеф. Наивное решение ищет соседа для каждой позиции отдельно и получает квадрат.

переворачивает задачу: вместо «для кого ищем» он держит тех, «кому ещё не ответили». Новый элемент закрывает сразу всех, кто меньше него, — и закрывает навсегда, потому что для будущих позиций он и ближе, и не меньше. Каждый индекс входит в один раз и выходит один раз, поэтому проход линеен.

Подробный разбор

10 подтем — объяснения, формулы, примеры и интерактивные графики.

Задача звучит безобидно: для каждого дня узнать, через сколько дней метрика впервые станет больше. Наивно — от каждого дня идти вперёд до первого большего значения. На убывающем ряде это : миллион точек превращается в операций.

В ту же форму укладываются максимальный прямоугольник в гистограмме, сбор дождевой воды и «максимум внутри »: для каждой позиции ищется ближайший подходящий сосед, и перебор соседей даёт квадрат.

На практике

Хранить только те элементы, для которых ответ ещё не найден. Пришедший элемент закрывает сразу всех, кто меньше него, — они уже никогда не станут ответом ни для кого.

при этом сам собой оказывается монотонным: всё, что нарушало бы порядок, из него уже снято. Отсюда и название, и линейное время.

день:      0    1    2    3    4    5
темп:     73   74   75   71   69   72

день 0:   стек [73]
день 1:   74 > 73  ->  ответ дня 0 = день 1      стек [74]
день 2:   75 > 74  ->  ответ дня 1 = день 2      стек [75]
день 3:   71 < 75                                стек [75, 71]
день 4:   69 < 71                                стек [75, 71, 69]
день 5:   72 > 69  ->  ответ дня 4 = день 5
          72 > 71  ->  ответ дня 3 = день 5
          72 < 75                                стек [75, 72]

у дней 2 и 5 ответа нет -- стек остался непустым
В стеке лежат дни без ответа, значения по ним убывают

День 5 закрыл сразу два вопроса — за один свой шаг. Именно поэтому вложенный while внутри цикла не превращает решение в квадратичное: снимать можно только то, что раньше положили.

answer = [-1] * len(a)
stack = []                       # индексы, значения по ним убывают

for i, x in enumerate(a):
    while stack and a[stack[-1]] < x:
        j = stack.pop()          # для j нашёлся ближайший больший
        answer[j] = i
    stack.append(i)

return answer                    # -1 там, где большего справа нет
Ближайший больший справа: в стеке хранятся индексы
from collections import deque

dq = deque()                     # индексы, значения по ним убывают
for i, x in enumerate(a):
    while dq and a[dq[-1]] <= x:
        dq.pop()                 # этот уже никогда не станет максимумом
    dq.append(i)

    if dq[0] <= i - k:
        dq.popleft()             # вышел за левую границу окна

    if i >= k - 1:
        yield a[dq[0]]           # максимум текущего окна
Монотонный дек: максимум в окне длины k
  • Инвариант — значения по индексам в стеке (деке) монотонны; всё, что нарушало бы порядок, уже снято.
  • Время: каждый индекс попадает в структуру один раз и снимается не более одного раза.
  • Память в худшем случае (монотонный вход), у дека в окне.

Ключевое наблюдение: если пришёл элемент , а в стеке лежит меньший , то больше никому не пригодится. Любой будущий элемент, для которого мог бы быть ответом, стоит правее , а значит окажется к нему ближе и не меньше по величине. Выбрасывать безопасно навсегда, а не только на этом шаге.

Отсюда же линейность, и она амортизированная: внутренний while в сумме по всем итерациям выполняет не больше снятий, потому что каждое снятие соответствует одному добавлению. Считать сложность по виду кода («цикл в цикле — значит квадрат») здесь неверно, и это стоит проговорить вслух на собеседовании.

Сигнал в условииЧто применять
«Ближайший больший (меньший) справа или слева»
«Сколько дней (шагов) ждать до…», ответ — разность индексов
«Максимальный прямоугольник», «наибольшая площадь под гистограммой» с границами влияния
«Максимум или минимум внутри окна длины k»монотонный дек
Наивное решение звучит как «для каждого элемента ищем ближайший подходящий»почти всегда
Нужен k-й по величине, а не ближайшийэто уже , а не монотонная структура

Что ищемПорядок в стекеНаправление обхода
Ближайший больший справаубывающийслева направо
Ближайший больший слеваубывающийсправа налево
Ближайший меньший справавозрастающийслева направо
Ближайший меньший слевавозрастающийсправа налево
  • Гистограмма — для каждого столбца ищутся ближайшие меньшие слева и справа: это границы прямоугольника, в котором он минимален.
  • Прямоугольник из единиц в матрице — та же гистограмма, построчно накопленная.
  • Сбор дождевой воды — второе классическое решение, по уровням, вместо двух встречных указателей.
  • Удаление k цифр — снимаем со стека всё, что больше пришедшей цифры, пока лимит удалений не исчерпан.
  • Монотонный дек — та же идея с доступом с двух концов: слева уходит вышедшее из окна, справа — бесперспективное.

  • Хранить значения вместо индексов. Почти во всех задачах нужен ответ «через сколько шагов» или границы отрезка — без индексов их не восстановить.
  • Строгое и нестрогое сравнение. < и <= различаются на дубликатах: одно даёт первый из равных, другое последний. Для гистограммы это влияет на двойной подсчёт площади.
  • Незакрытый остаток. После цикла в стеке лежат элементы, для которых ответа нет; их нужно обработать явно — или заранее добавить в конец фиктивный бесконечный элемент.
  • Дек не чистится слева. Максимум в окне вернёт устаревшее значение, если забыть снимать индексы, вышедшие за левую границу.
  • Худший случай по памяти. На монотонном входе в стеке окажется весь массив: памяти — это нормально, но об этом стоит сказать заранее.

ПриёмКогда он лучшеЦена
Кучанужен k-й по величине или минимум при произвольных удалениях на операцию
Скользящее окноагрегат окна — сумма или счётчик, а не максимуммонотонная структура не нужна
Дерево отрезковзапросы максимума на произвольных отрезках, а не на окне на запрос, больше кода
Сортировкапорядок дней или позиций не важентеряется то, ради чего задача и ставилась
На практике

Максимум в окне решает и — за . Дек даёт ровно потому, что не хранит того, что уже не пригодится, вместо того чтобы уметь это удалять.

  1. Почему вложенный while не делает решение квадратичным?
  2. Что лежит в стеке: значения или индексы — и почему это не вопрос вкуса?
  3. Ближайший меньший слева: какой порядок в стеке и в какую сторону идти?
  4. Максимальный прямоугольник в гистограмме [2, 1, 5, 6, 2, 3] — разберите руками.
  5. Максимум в окне: по каким двум причинам индекс уходит из дека?

Чем заменяют

Видеолекции

Тайм-кода именно на эту тему в записях нет. Но глава «Алгоритмы и структуры данных» разобрана в курсе целиком — с той оговоркой, что место в записи придётся искать самому.

Связанные темы

Два указателя и окно

Two Pointers and Sliding Window90%

Два указателя: семейство приёмов · Алгоритмы и структуры данных

Зонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.

Opposite-Direction Pointers90%

Встречные указатели · Алгоритмы и структуры данных

Два индекса сходятся с концов отсортированного массива. На каждом шаге отбрасывается конец, который заведомо не даст лучшего ответа, — и вместе с ним целая полоса пар.

Sliding Window90%

Скользящее окно · Алгоритмы и структуры данных

Окно между двумя границами: правая расширяет, левая чинит. Состояние не пересчитывается, а обновляется инкрементально — отсюда линейное время на задачах о подотрезках.

Fast and Slow Pointers90%

Быстрый и медленный указатель · Алгоритмы и структуры данных

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

Same-Direction Pointers90%

Два указателя в одну сторону · Алгоритмы и структуры данных

Слияние, пересечение и запись на месте: каждому источнику свой указатель, ни один не откатывается назад. Основа сортировки слиянием и merge join в базах данных.

Prefix Sums90%

Префиксные суммы · Алгоритмы и структуры данных

Один предварительный проход превращает сумму на любом отрезке в одно вычитание. Тот же приём вместе с хеш-таблицей находит подотрезок с заданной суммой за линейное время.

Проверить себя

Тема встречается в тесте по направлениям — ошибки приведут обратно на эту страницу.

Следующий шагПроверить себя: Алгоритмы и структуры данныхТема встречается в этом тесте 1 раз. Ошибка приведёт обратно на эту страницу — с объяснением, что именно не сошлось.Перейти →

Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.