Алгоритмы на массивах · модуль 10 · окно переменного размера
Глава 10.9
Ошибки
и объяснение решения
В этой главе собраны ошибки модуля 10 и то, как решение объясняют интервьюеру. Ошибки разобраны в главах 10.3–10.8, каждая с прогоном ошибочной и исправленной версии. Здесь они сведены в таблицу по симптомам: что видно в ответе, где искать в коде, на каком входе ошибка проявляется и где её прогон. Затем — четыре новые ошибочные версии для самостоятельного поиска, функция с двумя ошибками сразу и фразы, которыми объясняют три вида задач модуля.
- Категория
- Отладка и объяснение
- Уровень
- Средний
- Опирается на
- Главы 10.3–10.8; глава 9.9: ошибки модуля 9
- Зачем это знать
- Находить ошибку окна по симптому и объяснять условие while на секции
§1Сводка ошибок по симптомам
Когда тест падает, первым видно неверный ответ или исключение. Строку с ошибкой по ним ещё нужно найти. В таблице ошибки модуля сгруппированы по тому, как они выглядят снаружи. По симптому находят строку кода, которую нужно проверить, и вход, на котором ошибку видно. В последнем столбце — где эта ошибка запущена по шагам вместе с исправленной версией.
| Симптом | Где искать | Вход, который её показывает | Прогон |
|---|---|---|---|
| Самый длинный отрезок длиннее верного, в нём нарушено условие | Запись длины до цикла while | Элемент, после которого окно приходится сжимать: [4, 2, 5, …], лимит 7 | 10.3, §9 |
Окно сжимается не там, где нужно; на одних входах ответ меньше, на других IndexError | Порядок в теле while: l += 1 до вычитания | [9, 1], лимит 8 | 10.3, §9 |
| Самый короткий отрезок короче верного, его сумма меньше порога | Запись длины после цикла while | Любой вход с подходящим отрезком | 10.4, §6 |
| Самый короткий отрезок длиннее верного | Строгий знак: while total > s | Сумма лучшего отрезка точно равна порогу: [2, 3, 1, 2, 4, 3], 7 | 10.4, §6 |
| Ответ всегда 0 | best = 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], порог 0 | 10.6, §2 |
| Длина всего списка там, где отрезка нет | Начальное значение best = len(nums) | Сумма списка меньше порога | 10.6, §2 |
| 1 на пустой строке | Начальное значение best = 1 | Пустая строка | 10.6, §2 |
Буквы разного регистра считаются повтором; KeyError | text[r].lower() в расширении | 'aA' и 'Aa' | 10.6, §2 |
Окно сжимается до пустого, счётчики уходят в минус, IndexError | Нет del для ключа со счётчиком 0, условие считает ключи | [1, 2, 3, 2, 2] | 10.8, §9 |
Ошибки таблицы связаны с четырьмя местами кода. Первое — условие цикла while: знак, его направление, замена цикла на if. Второе — место записи ответа относительно цикла. Третье — порядок строк в сжатии и то, какой элемент из состояния убирается. Четвёртое — начальное значение ответа. Поэтому граничные входы из главы 10.6 и примеры с точным порогом и повторами находят почти все ошибки таблицы, а пример из условия задачи — только часть.
§2Найдите ошибку
Четыре версии ниже в главах не разбирались. В первых трёх вопросах выберите вход, на котором функция ошибается; на остальных вариантах её ответ верный. Четвёртая функция отвечает верно на любом входе — найдите, что с ней не так. Все ответы проверены запуском.
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элементов.
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 верный.
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 верный.
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). Ошибка в ответе: повтор может остаться в окне.Верно. Перед повторяющимся символом в окне могут стоять другие, и их нужно убрать по одному. Сколько раз, заранее неизвестно, поэтому нужен цикл.