Два индекса сходятся с концов отсортированного массива. На каждом шаге отбрасывается конец, который заведомо не даст лучшего ответа, — и вместе с ним целая полоса пар.
Ключевые тезисы
- Пара или тройка с условием на сумму за O(n) вместо O(n²) — при условии, что вход упорядочен.
- Корректность доказывается явно: назвать отброшенные пары и показать, что лучшей среди них нет.
- Память O(1): в задачах вроде сбора дождевой воды это решает, когда не помещается.
Какую задачу решает
Задачи про пару элементов с условием на сумму, про площадь между стенками, про палиндром — все они в лоб решаются перебором пар за . Если вход упорядочен, такой перебор проверяет заведомо безнадёжные варианты: порядок во входе есть, а решение им не пользуется.
Встречные указатели пользуются. Два индекса стоят на концах и на каждом шаге отбрасывают тот конец, который гарантированно не входит в лучший ответ, — а вместе с ним и все пары с его участием. Получается шагов вместо и памяти.
Подробный разбор
Задача «найти в массиве пару с суммой X» решается в лоб двумя вложенными циклами: пар. На тысяче элементов это миллион операций и доли секунды, на миллионе — операций и часы. При этом если массив отсортирован, перебор ведёт себя так, будто порядка нет: он проверяет пары, про которые заранее известно, что они не подходят.
То же самое в задачах «наибольшая площадь между стенками», «сколько воды соберётся», «палиндром ли строка»: наивное решение сравнивает всё со всем, хотя структура входа позволяет за один шаг отбрасывать сразу целые группы вариантов.
Если один из концов заведомо не может участвовать в лучшем ответе, его можно выбросить — вместе со всеми парами, в которые он входит.
Отсюда весь алгоритм: ставим указатели на концы и на каждом шаге решаем, какой конец «безнадёжен». Сдвиг одного указателя убирает из рассмотрения не один вариант, а целую полосу — поэтому шагов оказывается , а не .
индекс 0 1 2 3
высота 2 8 5 7
#
# #
# # #
# # # #
---------------
L R
площадь = min(2, 7) * (3 - 0) = 6| Шаг | L | R | min(высот) | Ширина | Площадь | Кого двигаем |
|---|---|---|---|---|---|---|
| 1 | 0 (2) | 3 (7) | 2 | 3 | 6 | L: левая стенка ниже |
| 2 | 1 (8) | 3 (7) | 7 | 2 | 14 | R: правая ниже |
| 3 | 1 (8) | 2 (5) | 5 | 1 | 5 | R: правая ниже |
| 4 | 1 (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нужно только там, где одиночный элемент допустим. - Дубликаты в тройках. Без явного пропуска повторов одна и та же тройка попадает в ответ несколько раз.
- Сортировка ломает индексы. Если в ответе нужны позиции из исходного массива, сортировать приходится пары «значение, индекс».
- Переполнение при сумме. В языках с ограниченным целым типом сумма двух больших элементов переполняется раньше, чем сравнивается с целью.
| Приём | Когда он лучше | Цена |
|---|---|---|
| Хеш-таблица | массив не отсортирован, нужны исходные индексы | памяти |
| Бинарный поиск | ищем дополнение для каждого элемента по отдельности | вместо |
| Скользящее окно | нужны подряд идущие элементы, а не пара | другое семейство задач |
| плюс встречные указатели | исходный порядок не нужен | на |
Если массив уже отсортирован, встречные указатели почти всегда выигрывают у не асимптотикой, а константой: последовательное чтение памяти против прыжков по хешу.
- Почему в задаче о воде нужно двигать меньшую стенку, а не большую?
- Пара с суммой X в отсортированном массиве — напишите цикл и назовите инвариант.
- Проверка палиндрома с пропуском не-букв: где здесь может возникнуть ошибка на единицу?
- Три числа с нулевой суммой: как избежать повторов в ответе, не используя множество?
- Сбор дождевой воды двумя указателями: почему достаточно хранить только два максимума?
Разобрать их руками полезнее, чем прочитать ещё одно объяснение: приём простой, а ошибок в нём делают много. Смежная проверка себя — в тесте по алгоритмам.
Чем заменяют
Видеолекции
Тайм-кода именно на эту тему в записях нет. Но глава «Алгоритмы и структуры данных» разобрана в курсе целиком — с той оговоркой, что место в записи придётся искать самому.
Связанные темы
Два указателя и окно
Two Pointers and Sliding Window90%
Два указателя: семейство приёмов · Алгоритмы и структуры данныхЗонтичная тема: что общего у четырёх приёмов с двумя индексами и как выбрать нужный. Сами приёмы разобраны каждый на своей странице.
Sliding Window90%
Скользящее окно · Алгоритмы и структуры данныхОкно между двумя границами: правая расширяет, левая чинит. Состояние не пересчитывается, а обновляется инкрементально — отсюда линейное время на задачах о подотрезках.
Fast and Slow Pointers90%
Быстрый и медленный указатель · Алгоритмы и структуры данныхДва указателя идут в одну сторону с разной скоростью. Если структура зациклена, быстрый догоняет медленного — цикл находится без журнала посещённых узлов.
Same-Direction Pointers90%
Два указателя в одну сторону · Алгоритмы и структуры данныхСлияние, пересечение и запись на месте: каждому источнику свой указатель, ни один не откатывается назад. Основа сортировки слиянием и merge join в базах данных.
Prefix Sums90%
Префиксные суммы · Алгоритмы и структуры данныхОдин предварительный проход превращает сумму на любом отрезке в одно вычитание. Тот же приём вместе с хеш-таблицей находит подотрезок с заданной суммой за линейное время.
Monotonic Stack and Deque90%
Монотонный стек и дек · Алгоритмы и структуры данныхПриём поверх стека: хранить только тех, для кого ответ ещё не найден. Даёт ближайший больший элемент для всех позиций сразу и максимум в скользящем окне — за один проход.
Всё, что названо в описании, разобрано здесь же или в соседней теме — переходы под каждым определением.
Безнадёжный конец
Тот, что ограничивает ответ: его сдвиг отбрасывает целую полосу пар.
Разбор ниже: Ключевая идеяДоказательство отбрасыванием
Назвать отброшенные пары и показать, что лучшей среди них нет.
Разбор ниже: Почему это работаетПредусловие сортировки
Без упорядоченности приём молча даёт неверный ответ.
Разбор ниже: Ограничения и типичные ошибкиТема встречается в тесте по направлениям — ошибки приведут обратно на эту страницу.
Глава «Алгоритмы и структуры данных» последний раз правилась . Нашли ошибку — напишите.