Приём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в — за один проход.
Ключевые тезисы
- Пришедший элемент закрывает вопрос сразу для всех меньших: они уже никогда не станут ответом.
- Каждый индекс кладётся и снимается один раз, поэтому вложенный while внутри цикла линеен, а не квадратичен.
- Четыре комбинации «больший или меньший, справа или слева» покрывают почти всё семейство задач.
Какую задачу решает
«Для каждого элемента найдите ближайший больший справа» — формулировка, за которой прячется целое семейство задач: сколько дней ждать потепления, какой прямоугольник максимален в гистограмме, сколько воды удержит рельеф. Наивное решение ищет соседа для каждой позиции отдельно и получает квадрат.
переворачивает задачу: вместо «для кого ищем» он держит тех, «кому ещё не ответили». Новый элемент закрывает сразу всех, кто меньше него, — и закрывает навсегда, потому что для будущих позиций он и ближе, и не меньше. Каждый индекс входит в один раз и выходит один раз, поэтому проход линеен.
Подробный разбор
Задача звучит безобидно: для каждого дня узнать, через сколько дней метрика впервые станет больше. Наивно — от каждого дня идти вперёд до первого большего значения. На убывающем ряде это : миллион точек превращается в операций.
В ту же форму укладываются максимальный прямоугольник в гистограмме, сбор дождевой воды и «максимум внутри »: для каждой позиции ищется ближайший подходящий сосед, и перебор соседей даёт квадрат.
Хранить только те элементы, для которых ответ ещё не найден. Пришедший элемент закрывает сразу всех, кто меньше него, — они уже никогда не станут ответом ни для кого.
при этом сам собой оказывается монотонным: всё, что нарушало бы порядок, из него уже снято. Отсюда и название, и линейное время.
день: 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]] # максимум текущего окна- Инвариант — значения по индексам в стеке (деке) монотонны; всё, что нарушало бы порядок, уже снято.
- Время — : каждый индекс попадает в структуру один раз и снимается не более одного раза.
- Память — в худшем случае (монотонный вход), у дека в окне.
Ключевое наблюдение: если пришёл элемент , а в стеке лежит меньший , то больше никому не пригодится. Любой будущий элемент, для которого мог бы быть ответом, стоит правее , а значит окажется к нему ближе и не меньше по величине. Выбрасывать безопасно навсегда, а не только на этом шаге.
Отсюда же линейность, и она амортизированная: внутренний while в сумме по всем итерациям выполняет не больше снятий, потому что каждое снятие соответствует одному добавлению. Считать сложность по виду кода («цикл в цикле — значит квадрат») здесь неверно, и это стоит проговорить вслух на собеседовании.
| Сигнал в условии | Что применять |
|---|---|
| «Ближайший больший (меньший) справа или слева» | |
| «Сколько дней (шагов) ждать до…» | , ответ — разность индексов |
| «Максимальный прямоугольник», «наибольшая площадь под гистограммой» | с границами влияния |
| «Максимум или минимум внутри окна длины k» | монотонный дек |
| Наивное решение звучит как «для каждого элемента ищем ближайший подходящий» | почти всегда |
| Нужен k-й по величине, а не ближайший | это уже , а не монотонная структура |
| Что ищем | Порядок в стеке | Направление обхода |
|---|---|---|
| Ближайший больший справа | убывающий | слева направо |
| Ближайший больший слева | убывающий | справа налево |
| Ближайший меньший справа | возрастающий | слева направо |
| Ближайший меньший слева | возрастающий | справа налево |
- Гистограмма — для каждого столбца ищутся ближайшие меньшие слева и справа: это границы прямоугольника, в котором он минимален.
- Прямоугольник из единиц в матрице — та же гистограмма, построчно накопленная.
- Сбор дождевой воды — второе классическое решение, по уровням, вместо двух встречных указателей.
- Удаление k цифр — снимаем со стека всё, что больше пришедшей цифры, пока лимит удалений не исчерпан.
- Монотонный дек — та же идея с доступом с двух концов: слева уходит вышедшее из окна, справа — бесперспективное.
- Хранить значения вместо индексов. Почти во всех задачах нужен ответ «через сколько шагов» или границы отрезка — без индексов их не восстановить.
- Строгое и нестрогое сравнение.
<и<=различаются на дубликатах: одно даёт первый из равных, другое последний. Для гистограммы это влияет на двойной подсчёт площади. - Незакрытый остаток. После цикла в стеке лежат элементы, для которых ответа нет; их нужно обработать явно — или заранее добавить в конец фиктивный бесконечный элемент.
- Дек не чистится слева. Максимум в окне вернёт устаревшее значение, если забыть снимать индексы, вышедшие за левую границу.
- Худший случай по памяти. На монотонном входе в стеке окажется весь массив: памяти — это нормально, но об этом стоит сказать заранее.
| Приём | Когда он лучше | Цена |
|---|---|---|
| Куча | нужен k-й по величине или минимум при произвольных удалениях | на операцию |
| Скользящее окно | агрегат окна — сумма или счётчик, а не максимум | монотонная структура не нужна |
| Дерево отрезков | запросы максимума на произвольных отрезках, а не на окне | на запрос, больше кода |
| Сортировка | порядок дней или позиций не важен | теряется то, ради чего задача и ставилась |
Максимум в окне решает и — за . Дек даёт ровно потому, что не хранит того, что уже не пригодится, вместо того чтобы уметь это удалять.
- Почему вложенный
whileне делает решение квадратичным? - Что лежит в стеке: значения или индексы — и почему это не вопрос вкуса?
- Ближайший меньший слева: какой порядок в стеке и в какую сторону идти?
- Максимальный прямоугольник в гистограмме [2, 1, 5, 6, 2, 3] — разберите руками.
- Максимум в окне: по каким двум причинам индекс уходит из дека?
Чем заменяют
Видеолекции
Тайм-кода именно на эту тему в записях нет. Но глава «Алгоритмы и структуры данных» разобрана в курсе целиком — с той оговоркой, что место в записи придётся искать самому.
Связанные темы
Два указателя и окно
Two Pointers and Sliding Window90%
Два указателя: семейство приёмов · Алгоритмы и структуры данныхЗонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.
Opposite-Direction Pointers90%
Встречные указатели · Алгоритмы и структуры данныхДва индекса сходятся с концов отсортированного массива. На каждом шаге отбрасывается конец, который заведомо не даст лучшего ответа, — и вместе с ним целая полоса пар.
Sliding Window90%
Скользящее окно · Алгоритмы и структуры данныхОкно между двумя границами: правая расширяет, левая чинит. Состояние не пересчитывается, а обновляется инкрементально — отсюда линейное время на задачах о подотрезках.
Fast and Slow Pointers90%
Быстрый и медленный указатель · Алгоритмы и структуры данныхДва указателя идут в одну сторону с разной скоростью. Если структура зациклена, быстрый догоняет медленного — цикл находится без журнала посещённых узлов.
Same-Direction Pointers90%
Два указателя в одну сторону · Алгоритмы и структуры данныхСлияние, пересечение и запись на месте: каждому источнику свой указатель, ни один не откатывается назад. Основа сортировки слиянием и merge join в базах данных.
Prefix Sums90%
Префиксные суммы · Алгоритмы и структуры данныхОдин предварительный проход превращает сумму на любом отрезке в одно вычитание. Тот же приём вместе с хеш-таблицей находит подотрезок с заданной суммой за линейное время.
Всё, что названо в описании, разобрано здесь же или в соседней теме — переходы под каждым определением.
Элементы без ответа
Содержимое стека: те, для кого ближайший больший ещё не найден.
Разбор ниже: Ключевая идеяАмортизация
Вложенный while линеен: снятий не больше, чем добавлений.
Разбор ниже: Почему это работаетЧетыре комбинации
Больший или меньший, справа или слева — порядок в стеке и направление обхода.
Разбор ниже: Вариации и параметрыТема встречается в тесте по направлениям — ошибки приведут обратно на эту страницу.
Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.