Модуль 10 · Окно переменного размера · ОП.04

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

Глава 10.8

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. У вас две корзины, в каждую можно класть фрукты только одного вида, сколько угодно штук. Вы начинаете с любого дерева, идёте вправо и с каждого дерева срываете ровно один фрукт. Если фрукт не подходит ни к одной корзине, сбор заканчивается. Верните наибольшее число фруктов, которое можно собрать».

Пересказ LeetCode 904 · Fruit Into Baskets, Medium; метки Array, Hash Table, Sliding Window и ограничения проверены по данным сайта 14.10.2026

Пересказ делается по правилам, по одному выводу на правило. Сравнение из жизни помогает удержать их вместе: сборщик — это рамка, которую ведут вдоль ряда, а корзины — ограничение на то, что может оказаться внутри рамки.

ПравилоНачать с любого дерева, идти вправо, срывать с каждого дерева
→
ВыводСобранные фрукты — деревья с индекса l по индекс r подряд, без пропусков. Это подотрезок.
ПравилоДве корзины, в каждой фрукты одного вида
→
ВыводВ подотрезке не больше двух разных чисел. Это условие допустимости.
ПравилоНаибольшее число фруктов
→
ВыводПо фрукту с дерева, поэтому число фруктов — длина подотрезка. Нужен самый длинный.

Пересказ для интервьюера: «Нужно найти самый длинный подотрезок списка, в котором не больше двух разных чисел, и вернуть его длину». После этой фразы задача узнаётся по признакам из главы 10.2: участок подряд, длина не задана, на содержимое стоит ограничение. Ограничение — число разных значений, поэтому состояние окна — частотная карта, как в главе 10.5.

§3Уточнения

Ответы интервьюера взяты из ограничений LeetCode. Задайте вопросы по очереди.

Две корзины
условие от интервьюера

«Деревья в ряд, fruits[i] — вид фрукта. Две корзины, в каждой один вид. Начать с любого дерева, идти вправо, срывать с каждого. Вернуть наибольшее число фруктов».

уточнено

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

    §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⁹ шагов, слишком долго».

    перебор, если его просят записатьвремя O(n²) · память O(1)
    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.

    1. Расширение. count[f] = count.get(f, 0) + 1 для вида f пришедшего дерева.
    2. Сжатие. Пока len(count) > 2: уменьшить счётчик вида на левом дереве. Если счётчик стал 0, удалить ключ: вида в окне больше нет, корзина освободилась. Затем l += 1.
    3. Запись. 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 МБ.

    решениевремя O(n) · память O(1)
    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).

    • Если вид в окне один, ключ один. А сразу после прихода третьего вида ключей три, пока цикл не сожмёт окно.

    Источники главы
    1. LeetCode 904. Fruit Into Baskets — leetcode.com/problems/fruit-into-baskets; условие, примеры, ограничения и метки сверены по данным сайта 14.10.2026
    2. КТП ОП.04, занятие 10: контроль «кандидат объясняет условие while и состояние окна»
    Модуль 10 · Окно переменного размера · ОП.04 · 3 курс Автор: Макаров Максим Николаевич