Модуль 8 · Два указателя · ОП.04

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

Глава 8.5

8.5

Почему указатели
не пропускают ответ

В этой главе алгоритм из главы 8.4 рассматривается с другой стороны — как обход таблицы всех пар. В таблице видно, почему каждый ход исключает сразу строку или столбец и почему нужная пара при этом не пропадает. Затем то же рассуждение записано как инвариант с тремя проверками, из монотонности указателей выведена оценка O(n), а в конце показано, что происходит, если вход не отсортирован.

Категория
Доказательство
Уровень
Средний
Опирается на
Глава 8.4; занятие 7: инвариант прохода
Зачем это знать
Объяснить на интервью, почему решение верное

§1Таблица всех пар

В главе 8.4 правило сдвига проверено на примерах, но один вопрос остался открытым: почему цена, которую исключил ход указателя, не могла входить в ответ. Чтобы ответить на него, запишем все пары каталога в таблицу. Строка — левая цена prices[i], столбец — правая prices[j], в клетке — их сумма. Заполнена только часть над диагональю, где i < j. Пара (2, 3) и пара (3, 2) — одна и та же пара товаров, а на диагонали товар складывается сам с собой.

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

§2Ход указателей по таблице

Указатели i = 0 и j = n − 1 — это правый верхний угол таблицы: самая дешёвая цена с самой дорогой. Клетка с рамкой — текущая пара. Если её сумма меньше цели, то и все клетки левее в этой строке меньше цели. Строку вычёркивают целиком и переходят на строку ниже — это i += 1. Если сумма больше цели, то и все клетки ниже в этом столбце больше: вычёркивается столбец, указатель уходит влево — j -= 1.

Кнопка «ход указателей» делает один ход и объясняет, что вычеркнуто. Кнопка «перебор по порядку» для сравнения обходит клетки так, как это делают вложенные циклы, — строку за строкой.

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

Та же таблица для цели, которой в каталоге нет. Здесь указатели дойдут до диагонали, вычеркнув всю таблицу, и вернут None.

§3Инвариант задачи 1

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

Инвариантзадача 1 · указатели навстречу

Если в списке есть пара с суммой target, то хотя бы одна такая пара лежит на участке с индекса i по индекс j включительно. Оба её индекса не меньше i и не больше j.

ПроверкаВопросДля задачи 1
1 · ИнициализацияВерен ли инвариант до первой итерации?i = 0, j = n − 1: участок — весь список, любая пара лежит внутри.
2 · СохранениеЕсли верен перед шагом, верен ли после?При s < target цена prices[i] с любой ценой участка даёт не больше s, то есть меньше цели. Пары с ней нет, и после i += 1 нужная пара осталась внутри. Для s > target и j -= 1 — так же.
3 · ЗавершениеЧто даёт инвариант после цикла?Либо пара найдена и возвращена, либо i ≥ j: в участке не больше одного элемента, пары в нём нет. По инварианту её нет и во всём списке — ответ None верный.
На полях: где в доказательстве нужна сортировка

Только во второй проверке: «цена prices[i] с любой ценой участка даёт не больше s». Это верно, потому что prices[j] — самая большая цена участка. В неотсортированном списке крайняя правая цена не обязательно самая большая, и вторая проверка не проходит.

§4Монотонность указателей

Инвариант объясняет, почему указатели не теряют ответ. Второй вопрос, который задают на review, — почему они делают не больше n шагов. Ответ на него — ещё одно свойство алгоритма.

Монотонность указателейуказатель движется только в одну сторону

Указатель называют монотонным, если за всё время работы его значение меняется только в одну сторону: i только растёт, j только уменьшается. Ни на одном шаге указатель не возвращается к уже пройденной позиции.

Из монотонности следует оценка времени. i может вырасти не больше чем на n − 1, j — уменьшиться не больше чем на n − 1, а цикл заканчивается, когда они встретятся. Каждая итерация сдвигает хотя бы один из них, поэтому итераций не больше n − 1, а время — O(n).

Указатели монотонны в каждой задаче модуля, и в каждой из этого следует оценка O(n).

ЗадачаКак движутся указателиИтераций не больше
Пара с заданной суммойi только вправо, j только влевоn − 1
Удаление повторов, перенос нулейr и w только вправо, w ≤ rn
Сжатие последовательностиstart, r и w только вправоn шагов для r
Подпоследовательностьj по тексту и i по слову — только вперёддлина текста

У перебора монотонности нет: внутренний указатель j после каждого шага внешнего цикла возвращается к i + 1 и проходит конец списка заново. Поэтому проверок там n·(n − 1) / 2.

§5Неотсортированный вход

Проверим, что без сортировки указатели действительно ошибаются. Возьмём тот же каталог и поменяем местами две цены: 700 и 1200. Пара 700 + 900 = 1600 в списке осталась, на индексах 3 и 4.

Терминалpython -i · Python 3.12
>>> print(pair_sum([300, 450, 1200, 700, 900, 1500], 1600))
None
>>> pair_sum_brute([300, 450, 1200, 700, 900, 1500], 1600)
(3, 4)

Перебор пару нашёл, указатели — нет. Прогон показывает, на каком ходе она потерялась: при i = 1, j = 4 сумма 450 + 900 = 1350 меньше цели, и правило исключает цену 450. У 450 пары действительно нет. Но на следующем ходе сумма 1200 + 900 = 2100 больше цели, и правило исключает 900 — а 900 входит в ответ вместе с 700. Ход j -= 1 верен, только если prices[i] = 1200 — самая дешёвая из оставшихся цен, а на участке ещё есть 700.

Поэтому первый вопрос к любой задаче на указатели навстречу — отсортирован ли вход. Если нет и индексы не нужны, его сортируют: sorted(prices) стоит O(n log n). Если индексы нужны, выбирают словарь — глава 8.2, §4.

§6Как объяснить решение на интервью

Кроме кода интервьюер ждёт рассуждение, по которому видно, что решение верное. Всё, что доказано в этой главе, для задачи 1 укладывается в четыре фразы: это инвариант и три проверки, сказанные обычными словами.

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

Что показываетПравило сдвига и почему исключённая цена не нужна — вторая проверка инварианта.

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

Что показываетВторой случай правила: направление сдвига обосновано в обоих случаях.

«Указатели только сходятся и назад не возвращаются, каждый шаг исключает одну цену. Поэтому шагов не больше n − 1: время O(n), память O(1)».

Что показываетОценку сложности с обоснованием — этого Яндекс ждёт по описанию секции.

«Это работает, только если цены отсортированы. Если бы не были — взял бы словарь просмотренных цен».

Что показываетКандидат знает границы применимости техники и запасной вариант.

§7Проверьте себя

Вопрос 1В таблице пар текущая клетка голубая: сумма меньше цели. Что вычёркивают указатели и почему?

  • Ниже по столбцу суммы растут: левая цена в нижних строках больше. Среди них может быть ответ.

  • Верно. Строка — одна левая цена, а правее текущей клетки непроверенных пар не осталось. Ход i += 1 вычёркивает эту строку.

  • Тогда указатели ничем не отличались бы от перебора. Порядок позволяет вычеркнуть сразу весь ряд.

Вопрос 2В каком месте доказательства используется то, что список отсортирован?

  • Весь список охвачен при любом порядке элементов. Инициализация от сортировки не зависит.

  • Это следует из условия цикла, порядок элементов здесь не участвует.

  • Верно. Только поэтому из s < target следует, что с остальными ценами участка сумма тоже меньше цели.

Модуль 8 · Два указателя · ОП.04 · 3 курс Автор: Макаров Максим Николаевич