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

Sliding Window

Скользящее окно

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

Часть темы: Two Pointers and Sliding WindowДва указателя: семейство приёмов

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

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

Длина окна и инкрементальное обновление состояния при сдвиге границы

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

  • Каждый индекс входит в один раз и выходит один раз: вложенный while не делает решение квадратичным.
  • Четыре типа окна: фиксированное, переменное на максимум, переменное на минимум и со счётчиком.
  • Условие применимости — монотонность: отрицательные числа при условии на сумму ломают приём.

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

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

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

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

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

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

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

На практике

Не пересчитывать заново, а сдвигать границы и обновлять состояние инкрементально: что вошло — прибавить, что вышло — вычесть.

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

a:  2  3  1  2  4  3  1  2      цель: сумма >= 8

    [2  3  1  2]                сумма 8  -> длина 4
       [3  1  2  4]             сумма 10 -> длина 4
          [2  4  3]             сумма 9  -> длина 3
             [4  3  1]          сумма 8  -> длина 3
Массив из восьми чисел: окно ползёт слева направо
right после расширенияСуммаСдвиг leftЛучшая длина
0[2]2
1[2 3]5
2[2 3 1]6
3[2 3 1 2]8left → 1, сумма 64
4[3 1 2 4]10left → 2, сумма 74
5[1 2 4 3]10left → 4, сумма 73
6[4 3 1]8left → 5, сумма 43
7[3 1 2]63

Восемь шагов правой границы и пять сдвигов левой — тринадцать операций вместо тридцати шести пар. Ответ: три элемента, например .

left = 0
total = 0
best = inf

for right, x in enumerate(a):
    total += x                      # вошло в окно

    while total >= S:               # окно допустимо -- пробуем сжать
        best = min(best, right - left + 1)
        total -= a[left]            # вышло из окна
        left += 1

return 0 if best == inf else best
Переменное окно: расширяем, чиним, обновляем ответ
total = sum(a[:k])
best = total

for right in range(k, len(a)):
    total += a[right] - a[right - k]
    best = max(best, total)
Фиксированное окно длины k: сумма обновляется за константу
  • Время: каждый индекс входит в один раз и выходит один раз.
  • Память для суммы и для счётчика символов, где — размер алфавита.
  • Длина окнаright - left + 1; это выражение и есть место, где чаще всего ошибаются на единицу.

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

Отсюда же линейность, и она амортизированная, а не «по одному циклу»: вложенный while внутри for выглядит квадратично, но left за весь алгоритм сдвигается не более раз в сумме по всем итерациям.

На практике

Сломать это свойство легко: добавьте в массив отрицательные числа, и сумма перестанет быть монотонной по границе — , ставшее недопустимым, может снова стать допустимым при дальнейшем расширении. Приём в этом случае неверен, а не просто неэффективен.

Сигнал в условииЧто он означает
«Подряд идущие», «подотрезок», «подстрока»прямое указание на
«Самая длинная … , в которой выполняется условие»переменное на максимум
«Самый короткий … , в котором достигается …»переменное на минимум
Задана длина k фиксированной длины
«Не более k различных», «не более k нулей» со счётчиком внутри
Все значения неотрицательны, условие про суммумонотонность есть — применимо
На практике

И главный запрещающий сигнал: слово подпоследовательность вместо подотрезка. Оно разрешает пропуски, и задача уходит к динамике.

Тип окнаЧто заданоЧто ищемЗадача
Фиксированноедлина kагрегат в каждом окнемаксимальная сумма k подряд, скользящее среднее
Переменное, на максимумограничениесамое длинное допустимое подстрока без повторов
Переменное, на минимумцельсамое короткое допустимое минимальный подотрезок с суммой не меньше S
Со счётчикоммультимножество символовсколько окон подходитвсе анаграммы образца в строке
С декомнужен максимум внутри окнаагрегат, которого не даёт суммаскользящий максимум — монотонный дек
По времени, а не по индексам в секундахагрегат за последние Tпотоковые метрики, антифрод

Параметр, который стоит назвать явно, — состояние окна. Для суммы это одно число, для «без повторов» — счётчик символов, для анаграмм — частот, для максимума — дек индексов. Всё остальное в шаблоне не меняется.

  • Отрицательные числа при условии на сумму. Монотонность исчезает, даёт неверный ответ; нужны префиксные суммы с хеш-таблицей.
  • Длина окна. right - left + 1, а не right - left.
  • Забыли вычесть уходящий элемент. Состояние окна разъезжается с самим окном, и ошибка проявляется только на длинных входах.
  • `if` вместо `while` при сжатии. Одного сдвига левой границы часто недостаточно, чтобы снова стало допустимым.
  • Пустой ответ. Когда допустимого окна нет вовсе, ответом должен быть ноль или «нет», а не начальное значение inf.

ПриёмКогда брать егоВ чём разница
Префиксные суммы + хешесть отрицательные, нужна точная сумма kне требуют монотонности, но стоят памяти
Встречные указателиищется пара, а не отрезоквход отсортирован, идём с концов
Монотонный декнужен максимум внутри окнанадстройка над окном, а не замена
Динамикаэлементы не обязаны идти подрядсостояние вместо двух границ
Бинарный поиск по ответудлина окна монотонна по условиювнешний поиск плюс проверка окном

  1. Почему вложенный while не делает решение квадратичным?
  2. Максимальная сумма k подряд идущих — напишите обновление суммы за константу.
  3. Самая длинная подстрока без повторяющихся символов: что хранится в состоянии окна?
  4. Минимальный подотрезок с суммой не меньше S — а что изменится, если разрешить отрицательные числа?
  5. Все анаграммы образца в строке: как сравнивать с образцом, не пересобирая счётчик?
На практике

Четвёртый вопрос — ключевой: ответ «ничего не изменится» означает, что монотонность осталась непонятой. Проверить себя на смежных темах можно тестом по алгоритмам.

Чем заменяют

Видеолекции

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

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

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

Two Pointers and Sliding Window90%

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

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

Opposite-Direction Pointers90%

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

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

Fast and Slow Pointers90%

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

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

Same-Direction Pointers90%

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

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

Prefix Sums90%

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

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

Monotonic Stack and Deque90%

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

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

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

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

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

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