Сортировка и два указателя
Предварительная сортировка превращает неупорядоченный массив в последовательность, где легко сравнивать соседние элементы и отбрасывать целые группы неподходящих значений. В этой теме разберём, как сочетать сортировку массива и метод двух указателей при поиске пар, диапазонов и серий.
Зачем сортировать массив
Пусть дан массив чисел и требуется найти два элемента с заданным свойством: сумма равна \(S\), разность не превышает \(D\), произведение меньше порога или оба элемента лежат в одном диапазоне. Если проверять все пары, получится \(O(n^2)\) сравнений. После сортировки элементы расположены по возрастанию, поэтому порядок становится дополнительной информацией.
Сортировка — перестановка элементов массива так, чтобы для любых соседних позиций выполнялось \(a_i \le a_{i+1}\). Сами значения сохраняются, меняется только их порядок.
Для задач на пары чаще всего нужна сортировка по возрастанию. Она позволяет сравнить сумму крайних элементов с целью и понять, какой указатель нужно сдвинуть. Обычно сортировка занимает \(O(n\log n)\), а последующий проход — \(O(n)\).
В отсортированном массиве при увеличении левого указателя значение не уменьшается, а при увеличении правого не увеличивается. Поэтому границу подходящих элементов можно двигать только в одном направлении и не возвращаться назад.
Два указателя для поиска пары
Для поиска пары с суммой \(S\) поставим левый указатель \(L\) в начало массива, а правый \(R\) — в конец. Рассмотрим сумму \(a_L+a_R\). Если она меньше \(S\), нужно увеличить сумму: самый безопасный шаг — увеличить \(L\). Если сумма больше \(S\), уменьшаем её, сдвигая \(R\) влево. Если сумма равна \(S\), пара найдена.
Если \(a_L+a_R<S\), то для текущего \(L\) ни один элемент правее \(R\) не подходит: он ещё меньше или равен \(a_R\), значит сумма не увеличится до \(S\). Поэтому можно безопасно увеличить \(L\). Аналогично, при сумме больше \(S\) можно уменьшить \(R\).
sort(a) L = 0 R = n - 1 while L < R: s = a[L] + a[R] if s == S: вывести a[L], a[R] L = L + 1 R = R - 1 elif s < S: L = L + 1 else: R = R - 1
Если требуется только ответ «есть ли пара», после найденной пары можно завершить алгоритм. Если нужно посчитать все пары, важно уточнить условие: считаются ли одинаковые значения одной группой, а элементы с одинаковыми значениями — разными экземплярами. Для такой обработки полезна обработка массива двумя указателями и отдельное сжатие одинаковых элементов.
Массив отсортирован: \([1,3,4,7,9]\), требуется сумма \(10\). Указатели стоят на \(1\) и \(9\). Что делаем после проверки суммы?
Разобранный пример: количество пар с ограничением
Задача: дан отсортированный массив \([1,2,3,5,6,8,10]\). Сколько пар \((i,j)\), где \(i<j\), имеют сумму не больше \(10\)? Здесь не требуется перечислять только одну пару: нужно учесть все подходящие пары.
Фиксируем левый индекс \(L\) и двигаем правый \(R\) от конца. Если \(a_L+a_R\le10\), то из-за сортировки подходят и все элементы между \(L+1\) и \(R\): их значения не больше \(a_R\). Значит, при таком \(R\) добавляется сразу \(R-L\) пар.
Всего получили \(5+4+2=11\) пар. Важен не сам набор чисел, а переход: при каждом увеличении \(L\) правый указатель не возвращается вправо. Поэтому после сортировки подсчёт выполняется за \(O(n)\).
def count_pairs_at_most(a, limit): a.sort() left, right = 0, len(a) - 1 answer = 0 while left < right: if a[left] + a[right] <= limit: answer += right - left left += 1 else: right -= 1 return answer
Диапазоны и непрерывные группы
После сортировки удобно искать максимальный диапазон, в котором разность между крайним и первым элементом не превышает \(D\). Левый указатель обозначает начало текущего диапазона, правый — его конец. Пока \(a_R-a_L\le D\), расширяем диапазон вправо. Если условие нарушилось, двигаем \(L\) вправо, пока оно снова не станет верным.
Так находят максимальное число элементов, укладывающихся в отрезок длины \(D\), число пар с разностью не больше \(D\), а также группы элементов, близких по значению. Если требуется обработать все одинаковые значения как одну группу, применяют приём обработки групп одинаковых элементов: указатель пропускает весь блок равных элементов.
Диапазон — непрерывный фрагмент отсортированного массива \([L,R]\). Его ширина по значениям равна \(a_R-a_L\), а количество элементов — \(R-L+1\).
Для строк сортировка обычно выполняется не самих символов, а строк или пар «значение — исходный индекс». Если нужно сохранить исходные данные, используют сортировку по ключу. Для подсчёта серий одинаковых символов пригодится максимальная серия, а для группировки по условию — группировка по признаку.
Типовые схемы и границы
- Пара с точной суммой: при сравнении суммы с целью двигаем один из указателей; при совпадении фиксируем ответ.
- Пара с суммой не больше цели: если крайняя пара подходит, добавляем сразу \(R-L\) пар и увеличиваем \(L\).
- Максимальный диапазон: увеличиваем \(R\), а при нарушении условия сдвигаем \(L\) до восстановления условия.
- Группы одинаковых элементов: находим левую границу группы, затем пропускаем все равные значения.
- Тройка элементов: внешний цикл фиксирует первый элемент, а для оставшихся используется поиск пары; это связано со страницей поиск тройки элементов.
1. Использовать два указателя на неотсортированном массиве: доказательство монотонности тогда не работает. 2. Включить пару элемента с самим собой: условие обычно требует \(L<R\). 3. При подсчёте пар добавить одну пару вместо всех \(R-L\), когда крайняя пара подходит. 4. Перепутать разность индексов и разность значений: для диапазона нужно \(a_R-a_L\). 5. Забыть, что после сортировки изменились индексы; если нужны исходные позиции, храните их вместе со значениями.
Сначала запишите, как меняется проверяемая величина при движении каждого указателя. Если увеличение \(L\) только увеличивает сумму, а уменьшение \(R\) только уменьшает её, схема подходит. Если оба указателя могут менять величину непредсказуемо, одного сортирования недостаточно.
Оценка сложности и подготовка ответа
Сортировка требует памяти и времени в зависимости от используемого алгоритма, но в экзаменационных решениях обычно достаточно указать \(O(n\log n)\). Два указателя делают не более \(n\) шагов каждым, поэтому их часть имеет сложность \(O(n)\). При ручном решении полезно выписать таблицу с \(L\), \(R\), значениями \(a_L\), \(a_R\) и результатом проверки.
| Условие | Действие при выполнении | Действие при нарушении |
|---|---|---|
| a_L+a_R=S | зафиксировать пару | сдвинуть указатель по знаку сравнения |
| a_L+a_R≤S | добавить R−L пар | уменьшить R |
| a_R−a_L≤D | расширить вправо | увеличить L |
| a_L=a_R | обработать группу | перейти к следующему значению |
Проверь себя
Главное
- Сначала сортируйте массив, если дальнейшее решение использует порядок значений.
- Для точной суммы сравнивайте \(a_L+a_R\) с целью и сдвигайте один указатель.
- Для подсчёта пар «сумма не больше» добавляйте сразу \(R-L\), когда крайняя пара подходит.
- Для диапазона поддерживайте условие \(a_R-a_L\le D\) и двигайте указатели только вперёд.
- Всегда проверяйте границу \(L<R\) и сохраняйте исходные индексы, если они нужны в ответе.