О чём статья
Two pointers и sliding window — два близкородственных шаблона, которые часто называют общим термином «два указателя». Они дают линейные алгоритмы там, где наивный подход — квадратичный, и применяются в десятках типов задач: подсчёт пар с условием, поиск кратчайшего/длиннейшего подотрезка с условием, удаление дубликатов, слияние массивов.
Несмотря на близость, у этих шаблонов разные предпосылки и способы движения указателей. Two pointers требует отсортированных (или иначе монотонных) данных и обычно двигает левый и правый указатели «навстречу» или «параллельно». Sliding window работает на исходном (несортированном) массиве и расширяет окно вправо до нарушения инварианта, затем сжимает слева до восстановления. Перепутать эти случаи — частая ошибка, которая ведёт к WA на тестах с особым порядком данных.
Когда применять: сигналы в условии
- «Найти пару с условием». Сумма пар, разность пар, отношение — после сортировки решается двумя указателями навстречу за .
- «Длиннейший / кратчайший подотрезок со свойством». Если свойство монотонно (расширяем окно — свойство «портится»), подходит sliding window.
- «Сколько подотрезков с условием». Часто работает sliding window с фиксацией правого конца и подсчётом допустимых левых.
- Любое движение по двум массивам параллельно. Слияние, поиск общих элементов в отсортированных массивах.
- Тройные суммы, четверные. Сводятся к фиксации одного-двух элементов и применению two pointers к остатку.
Если ни одно условие не выполняется (свойство не монотонно при движении окна, например), two pointers не работает — нужны другие техники (бинпоиск, DP, структуры данных).
Базовый шаблон
Two pointers на отсортированном массиве
Идея в одну фразу: оба указателя двигаются независимо, в зависимости от текущего состояния пары .
отсортировать массив a
l ← 0
r ← n - 1
пока l < r:
если условие(a[l], a[r]) ... :
l ← l + 1 # либо r ← r - 1
иначе:
... # обработка совпадения / накопление ответа
Базовая идея: движение происходит в зависимости от того, как сравнивается текущая пара с целью.
Sliding window на любом массиве
Идея в одну фразу: окно расширяется вправо, пока инвариант выполнен; при нарушении — сжимается слева до восстановления.
l ← 0
для r от 0 до n - 1:
добавить a[r] в окно (обновить инвариант)
пока инвариант нарушен:
удалить a[l] из окна
l ← l + 1
# окно [l, r] валидно, можно обновить ответ
Базовая идея: правый указатель только растёт, левый — тоже только растёт (никогда не возвращается). Это даёт суммарную работу .
Почему оба работают
Two pointers — на отсортированных данных монотонность даёт следующее: если для сумма слишком велика, то любое движение r влево или l вправо уменьшит сумму. Это позволяет на каждом шаге однозначно сдвинуть один указатель.
Sliding window — суммарная работа линейна, потому что каждый элемент входит в окно ровно один раз (через r) и выходит не более одного раза (через l). Инвариант «свойство монотонно при расширении окна» — обязательное условие. Если расширение может восстанавливать свойство — sliding window не работает.
Задача-иллюстрация 1: сумма пар на отсортированном массиве (~CF 1300)
Условие
Дан массив и число . Найти количество пар индексов , , таких что .
Ограничения. , .
Применение шаблона
Сортируем массив. Дальше — классические two pointers: , . Сравниваем с :
- Если — нужна большая сумма, сдвигаем вправо.
- Если — нужна меньшая сумма, сдвигаем влево.
- Если — нашли пару. Считаем её и сдвигаем оба, аккуратно учитывая повторяющиеся значения.
Тонкий момент — учёт повторов: если и в окне ещё одинаковых значений, то пар внутри этого блока . Если , то пар , где — количество повторов значения слева, — справа.
Код решения
Что поменялось
Базовый шаблон two pointers применяется напрямую, но добавилась обработка повторяющихся значений. Без неё — пропустим пары вида при , либо посчитаем их неправильно. Это типичное усложнение шаблона: когда задача спрашивает «количество», а не «существует ли», работа с повторами — отдельная подзадача.
Задача-иллюстрация 2: длиннейшее окно без повторов (~CF 1500)
Условие
Дан массив . Найти длину длиннейшего подотрезка, в котором все элементы различны.
Ограничения. , .
Применение шаблона
Это типичный sliding window. Инвариант — «все элементы в окне различны». При расширении окна вправо инвариант может нарушиться (новый элемент уже был в окне). Тогда сжимаем слева до тех пор, пока повторяющийся элемент не уйдёт.
Поддерживаем set или dict (хешмап) текущих элементов окна — операции добавления и удаления за в среднем.
Код решения
Что поменялось
Здесь два указателя двигаются в одну сторону (оба слева направо), а не навстречу. Это и отличает sliding window от two pointers на отсортированных данных. Инвариант «уникальные элементы» — монотонный: сжатие окна не может его сломать (удаляем элементы — повторов становится меньше). Это критическое условие применимости sliding window.
Алгоритмическая сложность — амортизированно: каждый элемент входит в seen один раз и выходит не более одного раза.
Задача-иллюстрация 3: минимальное окно с покрытием суммы (~CF 1700)
Условие
Дан массив положительных целых чисел и число . Найти длину кратчайшего подотрезка, сумма которого . Если такого нет — вернуть .
Ограничения. , , .
Применение шаблона
«Сумма » — монотонное свойство при расширении окна (массив положительных, сумма только растёт). Подходит sliding window:
- Расширяем правый указатель, пока сумма .
- Как только сумма — пытаемся сжать окно слева, оставаясь . Сжимаем, пока возможно.
- Длина текущего окна — кандидат на минимум.
Главное условие — массив положительный. Если есть нули или отрицательные значения, сумма не монотонна, sliding window не работает (нужны префиксные суммы + бинпоиск или другой подход).
Код решения
Что поменялось
Главное отличие от задачи 2: здесь мы хотим минимум, а не максимум, и поэтому обновляем ответ внутри цикла сжатия (а не снаружи). Логика: правый указатель фиксируется, левый сжимает окно как можно сильнее, пока инвариант ещё выполнен; в этот момент текущая длина — кандидат.
В задаче 2 (поиск максимума) обновление было после сжатия — потому что нас интересует самое длинное валидное окно.
Это ключевой нюанс sliding window: где обновлять ответ — внутри цикла сжатия или снаружи — зависит от того, ищем мы минимум или максимум.
Типичные ошибки
- Применение two pointers на несортированных данных. Базовая ошибка. Two pointers (навстречу) требует монотонности; без сортировки решение даёт WA на специально подобранных тестах. Если массив не отсортирован и сортировать нельзя (важен исходный порядок) — это задача на sliding window, а не two pointers.
- Sliding window на немонотонном свойстве. «Сумма при отрицательных значениях», «среднее значение в окне», «количество различных строго» — здесь sliding window даёт WA, потому что свойство не монотонно при движении.
- Левый указатель сдвигается назад. Признак ошибки: суммарная работа или больше. Если в коде есть
l = l - 1или сбросl = 0внутри цикла — sliding window не работает, нужен другой алгоритм. - Обновление ответа в неверном месте. Для максимума — после полного сжатия (когда окно валидно). Для минимума — внутри сжатия (как только окно валидно, фиксируем длину и пытаемся сжать ещё). Перепутать — WA.
- Тип под сумму. При и сумма до —
long longв C++. В Python переполнения нет. unordered_setв C++ при анти-хеш-атаках. На Codeforces встречаются специальные тесты противunordered_set<int>. Лекарство —set<int>(логарифм быстрее или сравним на ), или ручной хеш с рандомным сидом.- Граница
< nили<= n. В кодеfor r in range(n)правый указатель идёт от 0 до ; вwhile l < rуказатели не сталкиваются. Часто путают в C++ с диапазонами на основеsize_t.
Когда НЕ подходит
- Свойство не монотонно при расширении/сжатии окна. Тогда sliding window не помогает; нужны префиксные суммы + хешмап (для «сумма = »), сегментные деревья, или другие структуры.
- Многоразовые запросы по разным окнам. Sliding window работает за один проход; если задача спрашивает « запросов вида “окно , дать ответ”» — нужна другая техника (offline-сортировка запросов, sqrt-разбиение, Мо).
- Поиск пары без сортировки и с обязательным порядком индексов. Если порядок важен ( и нельзя сортировать), two pointers навстречу не работает; обычно — хешмап «для каждого ищем среди уже виденных , ».
- Скользящее окно фиксированной длины + быстрый минимум/максимум. Это «sliding window minimum/maximum» — отдельная задача, решается монотонным деком (см. соседний разбор).
Связанные техники
- Бинпоиск по ответу. Когда «найти кратчайший подотрезок со свойством» решается через бинпоиск длины + проверка за . Sliding window часто даёт то же за напрямую, без бинпоиска.
- Префиксные суммы. Когда свойство «сумма равна » и значения могут быть нулевыми или отрицательными. Sliding window не работает, префикс-суммы + хешмап работают.
- Монотонный дек. Расширение sliding window до «максимум в окне фиксированной длины» или «динамический максимум в растущем окне».
- Three pointers / multiple pointers. В задачах вроде «триплеты с суммой » — фиксируем один элемент, дальше two pointers на остатке. Сложность .
Итого
- Техника: two pointers — на отсортированных данных, два указателя двигаются независимо; sliding window — на любых данных с монотонным свойством, оба указателя только растут.
- Сложность шаблона: после сортировки (если она нужна, ).
- Сигналы в условии: «пара с условием», «подотрезок со свойством», «кратчайший/длиннейший с инвариантом».
- Типичные ловушки: применение two pointers без сортировки, sliding window на немонотонном свойстве, движение левого указателя назад, обновление ответа не в том месте цикла сжатия.