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

Opposite-Direction Pointers

Встречные указатели

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

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

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

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

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

  • Пара или тройка с условием на сумму за O(n) вместо O(n²) — при условии, что вход упорядочен.
  • Корректность доказывается явно: назвать отброшенные пары и показать, что лучшей среди них нет.
  • Память O(1): в задачах вроде сбора дождевой воды это решает, когда не помещается.

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

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

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

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

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

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

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

На практике

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

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

индекс   0   1   2   3
высота   2   8   5   7

             #
             #       #
             #   #   #
         #   #   #   #
        ---------------
         L           R

площадь = min(2, 7) * (3 - 0) = 6
Площадь = min(высот) × расстояние между стенками
ШагLRmin(высот)ШиринаПлощадьКого двигаем
10 (2)3 (7)236L: левая стенка ниже
21 (8)3 (7)7214R: правая ниже
31 (8)2 (5)515R: правая ниже
41 (8)1 (8)0указатели сошлись, стоп

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

left, right = 0, len(a) - 1
best = 0

while left < right:
    best = max(best, value(a, left, right))

    if a[left] < a[right]:      # левый конец ограничивает ответ
        left += 1
    else:
        right -= 1

return best
Встречные указатели: общий каркас
  • Инвариант: лучший ответ лежит среди пар с обоими концами внутри отрезка [left, right].
  • Время: каждый шаг сдвигает один из указателей, а сдвигов не больше .
  • Память: хранятся два индекса и текущий лучший ответ.
  • Предусловие — массив отсортирован либо величина, по которой принимается решение, монотонна по границе.
На практике

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

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

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

На практике

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

Сигнал в условииНасколько надёжен
Массив отсортирован (или его можно отсортировать), ищется пара с условием на суммупрямое указание
«Найдите два (три) элемента, таких что…»прямое указание
Задача симметрична относительно концов: палиндром, площадь, водапрямое указание
Ответ зависит от минимума или максимума на концах отрезкасильный сигнал
Требуется памяти на массивекосвенный: отпадает
На практике

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

ВариантЗадачаЧем отличается
Пара с суммойдва числа с суммой X в отсортированном массиверешение по сравнению суммы с целью
Палиндромстрока читается одинаково с обеих сторонуказатели сравнивают символы, а не считают
Тройкатри числа с нулевой суммойвнешний цикл по первому, внутри встречные:
Сбор водысколько воды удержит рельефхранятся максимумы слева и справа
Три указателя трёх цветов, партиция Хоараграницы делят массив на три зоны

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

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

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

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

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

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

Чем заменяют

Видеолекции

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

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

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

Two Pointers and Sliding Window90%

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

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

Sliding Window90%

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

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

Fast and Slow Pointers90%

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

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

Same-Direction Pointers90%

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

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

Prefix Sums90%

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

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

Monotonic Stack and Deque90%

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

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

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

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

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

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