Алгоритмы на массивах · модуль 10 · окно переменного размера
Глава 10.8
Задача целиком:
решение на интервью
В предыдущих главах этапы решения разбирались по отдельности. В этой главе новая задача проходит все этапы подряд, как задачи в главах 8.11 и 9.8. Порядок этапов и темп — те же, что на алгоритмической секции. Задача — LeetCode 904: сборщик идёт вдоль ряда деревьев с двумя корзинами. В условии нет слов «подотрезок» и «окно». Решение начинается с пересказа, в котором правила сбора превращаются в условие допустимости окна. На этапе проверки в первой версии кода находится ошибка, которой не было в главе 10.5.
- Категория
- Разбор задачи
- Уровень
- Средний
- Опирается на
- Главы 10.2–10.7; глава 9.8: этапы interview canvas
- Зачем это знать
- Узнать окно в задаче, где оно не названо, и уложиться в 25 минут
§1Этапы и время
Порядок этапов — шаблон interview canvas из КТП, как в главах 8.11 и 9.8. Этапы: условие → уточнения → примеры → перебор → оптимизация → сложность → код → проверка. Ориентир — 25 минут на задачу.
| Минуты | Этап | Что получается к концу этапа |
|---|---|---|
| 0–3 | Условие | Правила сбора пересказаны как задача о подотрезке, интервьюер подтвердил |
| 3–5 | Уточнения | Известны размер, смысл чисел, формат ответа |
| 5–8 | Примеры | Шесть входов с ответами, среди них ряд из одного вида |
| 8–10 | Перебор | Названо верное медленное решение и его сложность |
| 10–14 | Оптимизация | Условие допустимости, состояние окна, условие while |
| 14–15 | Сложность | Время и память быстрого решения, интервьюер согласен |
| 15–20 | Код | Функция записана в редакторе |
| 20–25 | Проверка | Код прогнан руками на примере, найденная ошибка исправлена |
На условие здесь отведено на минуту больше, чем в главе 9.8. Условие длинное, в нём описаны правила сбора, и слова «подотрезок» в нём нет. Пока оно не пересказано как задача о подотрезке, перебор и окно предложить нельзя.
§2Условие и пересказ
Интервьюер читает условие:
«Деревья растут в ряд слева направо, fruits[i] — вид фрукта на дереве i. У вас две корзины, в каждую можно класть фрукты только одного вида, сколько угодно штук. Вы начинаете с любого дерева, идёте вправо и с каждого дерева срываете ровно один фрукт. Если фрукт не подходит ни к одной корзине, сбор заканчивается. Верните наибольшее число фруктов, которое можно собрать».
Пересказ делается по правилам, по одному выводу на правило. Сравнение из жизни помогает удержать их вместе: сборщик — это рамка, которую ведут вдоль ряда, а корзины — ограничение на то, что может оказаться внутри рамки.
l по индекс r подряд, без пропусков. Это подотрезок.Пересказ для интервьюера: «Нужно найти самый длинный подотрезок списка, в котором не больше двух разных чисел, и вернуть его длину». После этой фразы задача узнаётся по признакам из главы 10.2: участок подряд, длина не задана, на содержимое стоит ограничение. Ограничение — число разных значений, поэтому состояние окна — частотная карта, как в главе 10.5.
§3Уточнения
Ответы интервьюера взяты из ограничений LeetCode. Задайте вопросы по очереди.
Решение меняют первые два ответа. Первый определяет состояние окна: числа сравниваются только на равенство, поэтому сумма как состояние окна не годится. Нужна частотная карта. Второй подтверждает, что собранные деревья образуют подотрезок, и окно применимо.
§4Примеры
Шесть входов, записанных до кода. Первые три — примеры из условия LeetCode, остальные проверяют ответы на уточнения. Ответы получены запуском итогового решения и совпадают с перебором.
| fruits | Ответ | Что проверяет |
|---|---|---|
[1, 2, 1] | 3 | подходит весь ряд: два вида |
[0, 1, 2, 2] | 3 | начать выгоднее со второго дерева: [1, 2, 2] |
[1, 2, 3, 2, 2] | 4 | третий вид встречается внутри: [2, 3, 2, 2] |
[3, 3, 3, 1, 2, 1, 1, 2, 3, 3, 4] | 5 | лучший отрезок в середине: [1, 2, 1, 1, 2] |
[5, 5, 5] | 3 | один вид, вторая корзина пустая |
[1, 2, 1, 2, 1, 2] | 6 | два вида чередуются, подходит весь ряд |
§5Перебор
Перебор следует из пересказа и устроен как перебор подстрок в главе 10.5. Для каждого начала l отрезок удлиняется вправо, виды собираются в множество, и внутренний цикл прерывается, когда видов стало три. «Проверю каждое начало и пойду вправо до третьего вида. В худшем случае, когда третьего вида нет, это n·(n + 1)/2 шагов, O(n²). При 10⁵ деревьев это около 5·10⁹ шагов, слишком долго».
def total_fruit_brute(fruits): best = 0 for l in range(len(fruits)): kinds = set() for r in range(l, len(fruits)): kinds.add(fruits[r]) if len(kinds) > 2: break best = max(best, r - l + 1) return best
Прогон на третьем примере, [1, 2, 3, 2, 2], ответ 4. Множество kinds никогда не содержит больше трёх видов: на третьем цикл прерывается. Поэтому память перебора — O(1).
Перебор повторяет ту же работу, что в главе 10.5: при переходе к следующему началу множество собирается заново. При этом отрезок с началом l + 1 почти совпадает с уже пройденным.
§6Оптимизация: окно с частотной картой
Это задача на самый длинный отрезок, поэтому схема — из главы 10.3: расширить окно, сжать, пока оно недопустимо, записать длину после цикла while. Остаётся выбрать состояние окна и записать через него условие допустимости.
Условие — «в окне не больше двух разных видов». Частотная карта из главы 10.5 хранит для каждого вида, сколько деревьев этого вида в окне. Если в карте лежат только виды, которые в окне есть, то число разных видов — это число ключей, len(count). Условие допустимости — len(count) <= 2, условие цикла сжатия — len(count) > 2.
- Расширение.
count[f] = count.get(f, 0) + 1для видаfпришедшего дерева. - Сжатие. Пока
len(count) > 2: уменьшить счётчик вида на левом дереве. Если счётчик стал 0, удалить ключ: вида в окне больше нет, корзина освободилась. Затемl += 1. - Запись.
best = max(best, r - l + 1)после цикла.
В главе 10.5 ключи со счётчиком 0 оставались в словаре, и это не мешало: условие цикла читало счётчик одного символа. Здесь условие читает число ключей, поэтому ключ ушедшего вида нужно удалять. Что будет без удаления, показано в §9.
Python в этой строке: len(count) и del count[key]
len от словаря возвращает число ключей. Ключ со значением 0 тоже считается. Оператор del удаляет ключ вместе со значением, после этого len уменьшается на 1.
>>> count = {1: 0, 2: 1, 3: 1}
>>> len(count)
3
>>> del count[1]
>>> count
{2: 1, 3: 1}
>>> len(count)
2Сделайте проход сами на четвёртом примере. Под лентой показана частотная карта окна; виды, которых в окне не осталось, в ней не показываются.
§7Сложность
Сложность называют до кода.
| Часть решения | Время | Дополнительная память |
|---|---|---|
| Расширения: по одному на дерево | O(n) | — |
| Сжатия: левая граница проходит ряд один раз | O(n) за весь проход | — |
| Частотная карта | O(1) на операцию в среднем | не больше трёх ключей: O(1) |
| Всё решение | O(n) | O(1) |
Память здесь — O(1). В главе 10.5 она была O(m), по числу разных символов. Ключи ушедших видов удаляются, а третий ключ появляется только на время сжатия. Поэтому в карте никогда не бывает больше трёх ключей, сколько бы разных видов ни было в ряду.
§8Код
На секции код пишет кандидат, поэтому до готового кода напишите функцию сами в окне ниже. «Запустить тесты» проверяет её на шести примерах из §4, на пустом ряде и сравнивает с перебором на трёхстах случайных рядах; часть тестов заготовка проходит сразу. Первый запуск загружает Python в браузер, около 10 МБ.
def total_fruit(fruits): count = {} best = 0 l = 0 for r in range(len(fruits)): f = fruits[r] count[f] = count.get(f, 0) + 1 while len(count) > 2: count[fruits[l]] -= 1 if count[fruits[l]] == 0: del count[fruits[l]] l += 1 best = max(best, r - l + 1) return best
Порядок строк в теле цикла while важен. Проверка count[fruits[l]] == 0 и удаление стоят до l += 1: пока l не сдвинут, fruits[l] — вид ушедшего дерева. После сдвига эта же запись означала бы уже другое дерево.
§9Проверка
Готовый код проверяют на примерах из §4. Если сжатие написать так же, как в главе 10.5, без удаления ключа, проверка на третьем примере находит ошибку сразу. Ниже два прогона на одном входе: первая версия кода и исправленная.
первая версия: без удаления ключаIndexErrorНа шаге 18 из окна уходит единственное дерево вида 1, его счётчик становится 0, но ключ остаётся. На шаге 20 в окне [2, 3] два вида, а в карте три ключа, и цикл продолжает сжимать окно. Окно становится пустым, счётчики уходят в минус, и на шаге 33 индекс l = 5 выходит за конец ряда.
4На шаге 20 ключ вида 1 удалён, и на шаге 22 в карте два ключа: окно [2, 3] допустимо. Дальше окно только расширяется, на шаге 33 записан ответ 4.
Остальные примеры проверяют быстрее. На [5, 5, 5] в карте один ключ, цикл while не выполняется ни разу, ответ 3. На [1, 2, 1, 2, 1, 2] ключей всегда два, ответ — длина ряда. На четвёртом примере цикл while срабатывает три раза. Первый раз — когда после трёх троек приходит вид 2. Второй — когда после отрезка из единиц и двоек приходит вид 3. Третий — когда после двух троек приходит вид 4. Все ответы совпадают с таблицей в §4.
По ходу секции кандидат объясняет интервьюеру каждое решение. Ниже — три фразы для этой задачи и этап, к которому относится каждая.
«Сборщик срывает с каждого дерева подряд, значит собранные деревья — подотрезок. Две корзины — не больше двух разных чисел в нём. Ищу самый длинный такой подотрезок».
ЭтапУсловие: пересказ
«Состояние окна — частотная карта, в ней только виды, которые есть в окне. Число ключей — число корзин. Сжимаю, пока ключей больше двух, и удаляю ключ, когда счётчик стал нулём».
ЭтапОптимизация: условие while и состояние окна
«Каждое дерево входит в окно и выходит из него один раз — O(n). В карте не больше трёх ключей — память O(1)».
ЭтапСложность
§10Проверьте себя
Вопрос 1Какое правило условия делает собранные фрукты подотрезком?
Это правило ограничивает содержимое отрезка: не больше двух видов. Подряд ли идут деревья, оно не говорит.
Верно. Пропускать деревья нельзя, поэтому собранные деревья идут подряд от начала до места остановки.
Это правило задаёт, что ищется: самый длинный отрезок. Что отрезок непрерывный, следует из правила о каждом дереве.
Вопрос 2Почему здесь ключ со счётчиком 0 нужно удалять, а в главе 10.5 — нет?
Опускались: на шаге 47 прогона главы 10.5 счётчик
'a'стал 0. Это не мешало, потому что условие цикла читало другой счётчик.В главе 10.5 ответ верен и без удаления, это проверено сравнением с перебором. Удаление там экономит только память.
Верно.
len(count)учитывает и ключи со счётчиком 0. В главе 10.5 условиеcount[ch] > 1читало счётчик одного символа.
Вопрос 3В ряду 1000 разных видов. Сколько ключей может быть в карте одновременно?
Так было бы без удаления ключей. Ключи ушедших видов удаляются при сжатии.
Верно. После каждого цикла
whileключей не больше двух, расширение добавляет не больше одного. Память O(1).Если вид в окне один, ключ один. А сразу после прихода третьего вида ключей три, пока цикл не сожмёт окно.
- LeetCode 904. Fruit Into Baskets — leetcode.com/problems/fruit-into-baskets; условие, примеры, ограничения и метки сверены по данным сайта 14.10.2026
- КТП ОП.04, занятие 10: контроль «кандидат объясняет условие while и состояние окна»