Алгоритмы на массивах · модуль 8 · два указателя
Глава 8.11
Задача целиком:
решение на интервью
В предыдущих главах этапы решения разбирались по отдельности: вопросы — в одной главе, перебор и указатели — в другой, доказательство, код и сложность — в третьих. В этой главе новая задача проходит все этапы подряд, в том порядке и примерно с той скоростью, что на алгоритмической секции. Порядок этапов — шаблон interview canvas из программы курса.
- Категория
- Разбор задачи
- Уровень
- Средний
- Опирается на
- Главы 8.2–8.10
- Зачем это знать
- Уложить решение в 25 минут секции и ничего не пропустить
§1Этапы и время
В календарно-тематическом плане курса ход решения записан как шаблон interview canvas: условие → уточнения → примеры → brute force → оптимизация → сложность → код → проверка. Сложность называют до кода: так интервьюер соглашается с выбранным решением раньше, чем кандидат потратит время на его запись. Журнал Яндекс Образования советует иметь решение первой задачи примерно через полчаса, поэтому ориентир в таблице — 25 минут.
| Минуты | Этап | Что получается к концу этапа |
|---|---|---|
| 0–2 | Условие | Задача пересказана своими словами, интервьюер подтвердил |
| 2–5 | Уточнения | Известны порядок, размер, крайние случаи, формат ответа |
| 5–8 | Примеры | Пять входов с ответами, среди них крайние |
| 8–10 | Перебор | Названо верное медленное решение и его сложность |
| 10–15 | Оптимизация | Правило быстрого решения и объяснение, почему оно верное |
| 15–16 | Сложность | Время и память быстрого решения, интервьюер согласен |
| 16–21 | Код | Функция записана в редакторе |
| 21–25 | Проверка | Код прогнан руками на примерах, найденные ошибки исправлены |
§2Условие и пересказ
Интервьюер формулирует задачу коротко:
«Дан список цен подарков и бюджет. Нужно выбрать два разных подарка так, чтобы их суммарная цена была как можно больше, но не превышала бюджет. Верните эту сумму».
Первое действие — пересказать условие своими словами и дождаться подтверждения: «Нужно из всех пар разных подарков выбрать пару с наибольшей суммой среди тех, что укладываются в бюджет, и вернуть саму сумму, без номеров». Пересказ сразу показывает, одинаково ли понята задача: например, что возвращается сумма, а номера подарков не нужны.
§3Уточнения
Вопросы те же по смыслу, что в главе 8.3, но ответы другие — и от одного из них меняется решение. Задайте их по очереди.
Решение меняет первый ответ: цены не отсортированы. В задаче 1 порядок был дан, здесь его придётся создать самим.
§4Примеры
Пять входов, записанных до кода. Первый — обычный, остальные проверяют ответы на уточнения. Ответы получены запуском итогового решения.
| prices | budget | Ответ | Что проверяет |
|---|---|---|---|
[700, 250, 1200, 450, 900, 300] | 1400 | 1350 | обычный случай: 450 + 900 |
[500, 500] | 1000 | 1000 | сумма ровно в бюджет подходит |
[900, 800] | 1500 | −1 | единственная пара не укладывается |
[500] | 1000 | −1 | один подарок — пары нет |
[] | 1000 | −1 | пустой список |
§5Перебор
Перебор здесь достаточно назвать, писать его не обязательно: «Проверю все пары, из подходящих запомню наибольшую сумму. Это O(n²): при 10⁵ подарков около 5·10⁹ пар, это слишком долго». Интервьюер видит, что верное решение есть, и дальше разговор идёт о том, как его ускорить.
def best_pair_sum_brute(prices, budget): best = -1 for i in range(len(prices)): for j in range(i + 1, len(prices)): s = prices[i] + prices[j] if s <= budget: best = max(best, s) return best
§6Оптимизация
Задача похожа на задачу 1: пара, сумма, сравнение с числом. Разница в том, что ищется не точная сумма, а наибольшая допустимая, и вход не отсортирован. Сначала сортируем копию, потом ставим указатели на самую дешёвую и самую дорогую цену. Правило сдвига меняется так:
- Сумма не больше бюджета. Это кандидат в ответ — запоминаем, если он больше прежнего. Для левой цены лучшей пары уже не будет: правая цена самая дорогая из оставшихся, с любой другой сумма меньше. Левую цену исключаем:
i += 1. - Сумма больше бюджета. Правая цена не укладывается даже с самой дешёвой из оставшихся, значит, с остальными тоже. Её исключаем:
j -= 1. - Указатели встретились. Все цены исключены, в запомненном значении — лучшая сумма или −1.
Обоснование то же, что в главе 8.5: каждый ход исключает цену, у которой гарантированно нет лучшей пары среди оставшихся. Инвариант: «лучшая пара всего списка либо уже учтена в best, либо лежит на участке с i по j».
§7Сложность и сортировка
Сложность называют до кода. Здесь она складывается из двух частей, и её стоит разложить явно — об этом на review спрашивают отдельно.
| Часть решения | Время | Дополнительная память |
|---|---|---|
Сортировка копии sorted(prices) | O(n log n) | O(n) — новый список |
| Проход указателями | O(n) — указатели монотонны | O(1) |
| Всё решение | O(n log n) | O(n) |
Проход указателями быстрее сортировки, поэтому итоговую сложность задаёт сортировка: O(n log n) + O(n) = O(n log n). Это всё равно намного быстрее перебора: при n = 10⁵ перебор делает около 5·10⁹ проверок, а n·log₂n — около 1,7·10⁶. Если бы интервьюер разрешил менять вход, вместо копии подошёл бы prices.sort(): список сортируется на месте, копия не создаётся.
В задаче 1 вход был отсортирован по условию, и решение стоило O(n). Здесь сортировку делает сам кандидат, и она становится самой дорогой частью. Поэтому вопрос «отсортирован ли вход?» меняет не только способ, но и итоговую оценку: O(n) против O(n log n).
Следующий этап — код. На секции его пишет кандидат, поэтому до готового кода в §8 напишите функцию сами в окне ниже. «Запустить тесты» проверяет её на пяти входах из §4; часть из них заготовка проходит сразу.
§8Код
Код записывает правило из §6 и почти повторяет задачу 1. Отличий три: сортировка в начале, переменная best и нестрогое сравнение с бюджетом.
def best_pair_sum(prices, budget): prices = sorted(prices) i, j = 0, len(prices) - 1 best = -1 while i < j: s = prices[i] + prices[j] if s <= budget: best = max(best, s) i += 1 else: j -= 1 return best
Python в этой строке: max(best, s)
max от нескольких чисел возвращает наибольшее. Запись best = max(best, s) оставляет в best лучшее из старого значения и новой суммы.
>>> max(-1, 1150) 1150
Python в этой строке: sorted(a) и a.sort()
sorted(a) возвращает новый отсортированный список, исходный не меняется. a.sort() сортирует сам список на месте и возвращает None. Обе сортировки стоят O(n log n).
>>> a = [3, 1, 2] >>> b = sorted(a) >>> a, b ([3, 1, 2], [1, 2, 3]) >>> a.sort() >>> a [1, 2, 3] >>> print(a.sort()) None
Строка prices = sorted(prices) создаёт новый отсортированный список и связывает с ним имя prices внутри функции. Список вызывающего кода не меняется, как и требовал интервьюер.
§9Проверка
Готовый код проверяют на примерах из §4. На секции первый пример проходят руками, шаг за шагом; ниже тот же проход показан пошаговым прогоном. На первом шаге в ячейках исходный список, со второго — отсортированная копия.
Остальные примеры проверяют быстрее, без записи каждого шага. На [500, 500] сумма 1000 проходит по <=, ответ 1000. На [500] и [] цикл не выполняется, и функция возвращает −1. На [900, 800] единственная сумма 1700 больше бюджета, j сдвигается, указатели встречаются, ответ −1. Все пять ответов совпадают с таблицей в §4.
По ходу секции кандидат объясняет интервьюеру каждое решение. Ниже — три фразы для этой задачи и этап, к которому относится каждая.
«Цены не отсортированы, номера не нужны — поэтому сортирую копию, это O(n log n)».
ЭтапУточнения и выбор способа
«Если сумма в бюджете, для левой цены лучшая пара найдена — сдвигаю её. Если больше, правая цена не подходит ни с одной — сдвигаю её».
ЭтапОптимизация и обоснование
«Итого O(n log n) за сортировку плюс O(n) за проход, память O(n) на копию. Если вход можно менять — сортирую на месте».
ЭтапСложность
§10Проверьте себя
Вопрос 1Почему в этой задаче после суммы в пределах бюджета сдвигается указатель i?
Это пересказ правила. Причина — в том, что у левой цены лучшая пара уже найдена.
Верно. С любой другой оставшейся ценой её сумма меньше, то есть хуже для ответа. Левую цену можно исключить.
Скорость здесь ни при чём: неверный сдвиг сошёлся бы не медленнее, но потерял бы ответ.
Вопрос 2Какая итоговая сложность решения по времени?
Проход указателями действительно O(n), но перед ним стоит сортировка, и она дороже.
Так стоит перебор. Указатели сравнивают каждую цену не больше одного раза.
Верно. O(n log n) + O(n) = O(n log n): итоговую оценку задаёт самая дорогая часть.
Вопрос 3Интервьюер уточнил: «Цены уже отсортированы». Что меняется?
Верно. Остаётся только проход указателями: O(n) по времени и O(1) по памяти, как в задаче 1.
Лишняя сортировка сохранит верный ответ, но оставит O(n log n) и O(n) памяти без причины.
Перебор от порядка не ускоряется: пар по-прежнему n·(n − 1) / 2.