Алгоритмы на массивах · модуль 8 · два указателя
Глава 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 ≤ r | n |
| Сжатие последовательности | start, r и w только вправо | n шагов для r |
| Подпоследовательность | j по тексту и i по слову — только вперёд | длина текста |
У перебора монотонности нет: внутренний указатель j после каждого шага внешнего цикла возвращается к i + 1 и проходит конец списка заново. Поэтому проверок там n·(n − 1) / 2.
§5Неотсортированный вход
Проверим, что без сортировки указатели действительно ошибаются. Возьмём тот же каталог и поменяем местами две цены: 700 и 1200. Пара 700 + 900 = 1600 в списке осталась, на индексах 3 и 4.
>>> 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следует, что с остальными ценами участка сумма тоже меньше цели.