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

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

Глава 10.9

10.9

Ошибки
и объяснение решения

В этой главе собраны ошибки модуля 10 и то, как решение объясняют интервьюеру. Ошибки разобраны в главах 10.3–10.8, каждая с прогоном ошибочной и исправленной версии. Здесь они сведены в таблицу по симптомам: что видно в ответе, где искать в коде, на каком входе ошибка проявляется и где её прогон. Затем — четыре новые ошибочные версии для самостоятельного поиска, функция с двумя ошибками сразу и фразы, которыми объясняют три вида задач модуля.

Категория
Отладка и объяснение
Уровень
Средний
Опирается на
Главы 10.3–10.8; глава 9.9: ошибки модуля 9
Зачем это знать
Находить ошибку окна по симптому и объяснять условие while на секции

§1Сводка ошибок по симптомам

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

СимптомГде искатьВход, который её показываетПрогон
Самый длинный отрезок длиннее верного, в нём нарушено условиеЗапись длины до цикла whileЭлемент, после которого окно приходится сжимать: [4, 2, 5, …], лимит 710.3, §9
Окно сжимается не там, где нужно; на одних входах ответ меньше, на других IndexErrorПорядок в теле while: l += 1 до вычитания[9, 1], лимит 810.3, §9
Самый короткий отрезок короче верного, его сумма меньше порогаЗапись длины после цикла whileЛюбой вход с подходящим отрезком10.4, §6
Самый короткий отрезок длиннее верногоСтрогий знак: while total > sСумма лучшего отрезка точно равна порогу: [2, 3, 1, 2, 4, 3], 710.4, §6
Ответ всегда 0best = min(best, …) при best = 0Любой вход с подходящим отрезком10.4, §7
В подстроке-ответе есть повторif вместо while в сжатииПовтору предшествуют другие символы: 'abba'10.5, §7
Ответ больше верного, частотная карта не совпадает с окномcount[ch] -= 1 вместо count[text[l]] -= 1Слева уходит другой символ: 'pwwkew'10.5, §7
Левая граница сдвигается назадНет проверки last[ch] >= l в варианте с позициямиПовтор символа, который уже вышел из окна: 'abba'10.5, §8
IndexError при пороге 0 или меньшеНет проверки s < 1[1, 2], порог 010.6, §2
Длина всего списка там, где отрезка нетНачальное значение best = len(nums)Сумма списка меньше порога10.6, §2
1 на пустой строкеНачальное значение best = 1Пустая строка10.6, §2
Буквы разного регистра считаются повтором; KeyErrortext[r].lower() в расширении'aA' и 'Aa'10.6, §2
Окно сжимается до пустого, счётчики уходят в минус, IndexErrorНет del для ключа со счётчиком 0, условие считает ключи[1, 2, 3, 2, 2]10.8, §9

Ошибки таблицы связаны с четырьмя местами кода. Первое — условие цикла while: знак, его направление, замена цикла на if. Второе — место записи ответа относительно цикла. Третье — порядок строк в сжатии и то, какой элемент из состояния убирается. Четвёртое — начальное значение ответа. Поэтому граничные входы из главы 10.6 и примеры с точным порогом и повторами находят почти все ошибки таблицы, а пример из условия задачи — только часть.

§2Найдите ошибку

Четыре версии ниже в главах не разбирались. В первых трёх вопросах выберите вход, на котором функция ошибается; на остальных вариантах её ответ верный. Четвёртая функция отвечает верно на любом входе — найдите, что с ней не так. Все ответы проверены запуском.

Код 1Функция должна вернуть длину самого длинного отрезка с суммой не больше limit.
def longest_within(nums, limit):
    best = 0
    total = 0
    l = 0
    for r in range(len(nums)):
        total += nums[r]
        while total > limit:
            total -= nums[l]
            l += 1
        best = max(best, r - l)
    return best
  • Цикл не выполняется, ответ 0 верный.

  • Окно сжимается до пустого, r − l равно −1, max(0, −1) равно 0 — верный ответ: отрезка нет.

  • Верно. Окно [2] допустимо, его длина 1, а r − l равно 0. Функция возвращает 0 вместо 1: окно с l по r включительно содержит r − l + 1 элементов.

Код 2Функция должна вернуть длину самого короткого отрезка с суммой не меньше s. В условие цикла добавлено l < r, чтобы окно не становилось пустым.
def shortest_window(nums, s):
    best = 0
    total = 0
    l = 0
    for r in range(len(nums)):
        total += nums[r]
        while total >= s and l < r:
            length = r - l + 1
            if best == 0 or length < best:
                best = length
            total -= nums[l]
            l += 1
    return best
  • Подходит только весь список длины 3, и он записывается при l = 0, r = 2. Ответ 3 верный.

  • Верно. Окно из одного элемента [40] подходит, но при l = r условие цикла ложно, и длина 1 не записывается. Функция возвращает 2.

  • Сумма не достигает порога, цикл не выполняется, ответ 0 верный.

Код 3Функция должна вернуть длину самой длинной подстроки без повторов.
def longest_unique(text):
    count = {}
    best = 0
    l = 0
    for r in range(len(text)):
        ch = text[r]
        count[ch] = 1
        while count[ch] > 1:
            count[text[l]] -= 1
            l += 1
        best = max(best, r - l + 1)
    return best
  • Повторов нет, сжимать не нужно, ответ 3 верный.

  • Верно. Счётчик пришедшего символа каждый раз записывается равным 1, и условие цикла никогда не выполняется. Функция возвращает длину всей строки, 3 вместо 2.

  • Цикл не выполняется, ответ 0 верный.

Код 4Функция должна вернуть длину самого короткого отрезка с суммой не меньше s. Ответ верный на любом входе. Что с ней не так?
def shortest_window(nums, s):
    best = 0
    l = 0
    for r in range(len(nums)):
        while sum(nums[l:r + 1]) >= s:
            length = r - l + 1
            if best == 0 or length < best:
                best = length
            l += 1
    return best
  • На пустом списке цикл не выполняется, функция возвращает 0 — это верный ответ.

  • Условие >= s включает равенство. На 5000 случайных входах ответы совпали с верной версией.

  • Верно. Состояния окна нет: каждая проверка условия складывает срез длиной с окно. На 20 000 чисел от 1 до 9 с порогом 25 000 эта версия работала 488 мс, верная — 1,2 мс.

На полях: ошибка, которую тесты на правильность не находят

Код 4 проходит все тесты shortest_window: тесты проверяют ответ, а время не измеряют. Обе границы в нём монотонны, но шаг стоит не O(1). Такую ошибку находят review и замер: у каждой строки в цикле спрашивают, сколько она стоит. Ответ на вопрос «что хранится между итерациями» здесь — только l и best; суммы окна среди них нет.

§3Две ошибки в одной функции

В окне ниже total_fruit из главы 10.8 с двумя ошибками из сводки. В сжатии нет удаления ключа со счётчиком 0, и длина окна считается как r − l. На примерах она возвращает ответ на единицу меньше верного или заканчивается исключением IndexError либо KeyError. Исправьте обе ошибки. Сначала запустите тесты и посмотрите, какие проходят.

Тест test_empty заготовка проходит: на пустом ряде цикл не выполняется, и ни одна из двух ошибок не проявляется. Тесты test_example_1 и test_one_kind находят только вторую ошибку: в них не больше двух видов, и сжатия нет. Первую ошибку находят входы, где приходит третий вид.

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

Кроме кода интервьюер ждёт рассуждение, по которому видно, что решение верное и почему оно O(n). Контроль КТП занятия 10 называет два пункта этого рассуждения: условие while и состояние окна. Для трёх видов задач модуля объяснение укладывается в несколько фраз. Под каждой фразой подписано, что она показывает интервьюеру.

Самый короткий отрезок: сумма не меньше порога

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

Что показываетСостояние окна и условие while, выведенное из задачи.

«Числа неотрицательные, поэтому расширение сумму не уменьшает, а сжатие не увеличивает. Отброшенное начало с более правым концом даст только более длинный отрезок».

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

«Ноль в ответе значит "отрезка нет": настоящая длина не меньше 1. Порог меньше 1 — ошибка вызова, на него ValueError».

Что показываетНачальное значение и граничные входы.

Самый длинный отрезок: подстрока без повторов

«Состояние окна — частотная карта: для каждого символа окна, сколько раз он в нём есть. Расширение может создать повтор только у пришедшего символа, поэтому условие цикла — count[ch] больше 1».

Что показываетСостояние окна и почему достаточно проверить один счётчик.

«Здесь нужен цикл: одного сжатия может не хватить: в 'abba' после ухода a повтор b остаётся. Длину записываю после цикла — там окно без повторов».

Что показываетЗачем нужен while и где записывается ответ.

«While внутри for, но l только растёт и не обгоняет r. Сдвигов не больше 2n — время O(n), память O(m) по числу разных символов».

Что показываетВерная оценка вложенного цикла.

Окно, не названное в условии: две корзины

«Сбор с каждого дерева подряд — это подотрезок, две корзины — не больше двух разных значений. Ищу самый длинный такой отрезок».

Что показываетСведение условия к известной технике.

«Число видов — число ключей карты, поэтому ключ со счётчиком 0 удаляю. Без этого len(count) считал бы виды, которых в окне уже нет».

Что показываетСвязь состояния окна с условием цикла.

Во всех трёх случаях порядок одинаковый. Сначала — что хранится в состоянии окна и какое условие у цикла while. Затем — где записывается ответ, почему указатели не возвращаются и что происходит на границах. Этот порядок совпадает со схемой объяснения из главы 10.6, §7.

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

Вопрос 1Тест shortest_window([2, 3, 1, 2, 4, 3], 7) вернул 1, хотя ни один элемент списка не достигает 7. Какое место проверить первым?

  • Строгий знак пропускает окна с суммой точно 7 и даёт ответ длиннее верного. Ответ короче верного он не даёт.

  • Верно. Длина 1 записана для окна, которое не подходит. Так бывает, когда запись стоит после цикла: там сумма окна уже меньше порога.

  • Неверное начальное значение даёт 0 или длину списка. Длину 1 на этом входе оно не создаёт.

Вопрос 2Интервьюер спрашивает: «Почему в задаче о подстроке нельзя написать if вместо while?» Какой ответ полный?

  • Ответ не называет причину. Интервьюер ждёт вход, на котором if ошибается, и объяснение.

  • С if время остаётся O(n). Ошибка в ответе: повтор может остаться в окне.

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

Модуль 10 · Окно переменного размера · ОП.04 · 3 курс Автор: Макаров Максим Николаевич