EduBrick
классная работа

Поиск и подсчёты

Программирование на Python: от нуля до олимпиад

0/18решено 0 из 18до зачёта осталось 10

Чему научитесь

  • Пользоваться готовыми in, index и count и знать их цену
  • Считать частоты всех значений за один проход
  • Отвечать по таблице частот на вопросы об уникальных и повторах
  • Выбирать приём по ограничению на значения, а не только на количество

Как устроено занятие

Сначала разбор с примерами и короткими проверками понимания — они нужны, чтобы поймать непонятое сразу, а не через три темы.

Дальше 15 задач лестницей: разминка, основа, со звёздочкой. Занятие засчитывается, когда решено 10 — остальные не пропадают и учитываются отдельно.

После занятия — вторая часть, ещё 15 задач на те же приёмы в новых сюжетах.

Сколько это займёт

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

теория

Три готовых вопроса

К списку можно задать три вопроса, и на каждый есть готовый ответ.

a = [3, 8, 1, 8, 5]

print(8 in a)         # True — есть ли такое значение
print(a.index(8))     # 1 — номер первого вхождения, с нуля
print(a.count(8))     # 2 — сколько раз встречается

Всё это вы уже видели у строк: in, find, count. Разница в одном и важном:

index падает, если элемента нет. Не возвращает −1, как строковый find, а останавливает программу с ошибкой ValueError. Поэтому его либо предваряют проверкой,

if x in a:
    print(a.index(x) + 1)
else:
    print(0)

либо ищут обычным циклом по номерам. Второй способ надёжнее и нужен всё равно: готового «последнего вхождения» у списка нет.

теория

Чего эти средства стоят

Каждый из трёх вопросов кажется мгновенным, но внутри — обычный проход по списку. x in a перебирает элементы, пока не найдёт; count доходит до конца всегда.

На одном вызове это незаметно. А вот так — уже нет:

for x in a:
    if a.count(x) == 1:      # проход по всему списку на каждом шаге
        ...

Внешний цикл делает nn шагов, count внутри — ещё nn. Итого n2n^2: при ста тысячах элементов это десять миллиардов действий, и решение не проходит.

Ошибка коварна тем, что код выглядит коротким и аккуратным. Вложенности не видно — она спрятана внутри метода.

Правило: готовый метод внутри цикла по тому же списку — почти всегда квадрат. Если такое понадобилось, значит нужен другой приём. Он в следующем блоке.

тест

Проверка: сколько действий

В списке 100 000 элементов. Сколько примерно действий сделает этот код?

for x in a:
    if a.count(x) > 1:
        ...
Войдите, чтобы ответить.
теория

Массив счётчиков

Главный приём занятия. Если значения не слишком велики, частоты считаются за один проход.

Идея: завести список, где номер — это само значение, а хранится в нём количество.

table = [0] * 1001          # список из 1001 нуля: для значений от 0 до 1000

for x in a:
    table[x] += 1           # встретили значение x — отметили

print(table[5])             # сколько раз встретилась пятёрка

Запись [0] * 1001 создаёт список нужной длины, заполненный нулями. Один проход по данным — и известны частоты всех значений сразу.

Дальше из таблицы читается почти всё:

вопрос как ответить по таблице
сколько различных значений сколько счётчиков больше нуля
какое значение самое частое наибольший счётчик
какие встретились ровно раз счётчики, равные единице
сколько пар одинаковых сумма c(c1)/2c \cdot (c - 1) / 2 по всем счётчикам

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

теория

Когда приём не работает

У массива счётчиков есть цена: его длина равна количеству возможных значений, а не количеству данных.

Для значений от 0 до 1000 таблица занимает тысячу ячеек — ничто. Для значений до миллиона — миллион ячеек, всё ещё приемлемо. А для значений до миллиарда таблицу построить уже нельзя: памяти не хватит.

Поэтому в условиях задач этого занятия ограничение на значения указано отдельно от ограничения на количество — смотрите на него, оно и подсказывает решение:

в условии что делать
значения до тысячи, элементов сто тысяч массив счётчиков
значения до миллиарда, элементов тысяча можно перебирать пары
значения до миллиарда, элементов сто тысяч ни то ни другое — нужен приём из пятого модуля

Третий случай закрывают словари и множества: они считают частоты при любых значениях. До них мы дойдём в модуле про функции и словари, а пока задачи подобраны так, чтобы хватало счётчиков.

И ещё одно. Отрицательные значения индексами быть не могут: table[-3] в Python не ошибка, а обращение с конца списка — то есть тихо неверный ответ. Если в задаче есть отрицательные, их сдвигают: table[x + 1000] += 1.

расчёт

Проверка: размер таблицы

Значения в списке — целые числа от 0 до 100 включительно.

Сколько ячеек должно быть в массиве счётчиков? Введите целое число.

Войдите, чтобы ответить.
теория

Три ошибки этого занятия

Таблица на единицу короче. Для значений от 0 до mm нужно m+1m + 1 ячеек. Признак: list index out of range ровно на самом большом значении.

index без проверки. Элемента нет — программа падает с ValueError. Признак: падение на тестах, где искомого значения не оказалось.

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

Как проверять себя

  • все элементы одинаковые — различное значение одно, частота максимальная;
  • все элементы разные — каждая частота равна единице;
  • искомого нет — что выводится по условию;
  • крайние значения: 0 и верхняя граница диапазона — они проверяют размер таблицы.
теория

Практика: пятнадцать задач

Лестница прежняя: пять разминочных, семь основных, три со звёздочкой. Зачёт при десяти решённых.

Первые задачи решаются готовыми средствами, дальше почти везде выручает массив счётчиков. Обращайте внимание на ограничение значений: именно оно говорит, какой длины делать таблицу и годится ли она вообще.

В одной задаче со звёздочкой значения намеренно велики, а список короток — там счётчики не построить, и перебор пар оказывается уместным. Это не поблажка, а часть навыка: выбирать приём по ограничениям.

лестница задач
развёрнутый ответ

Короткое решение, которое не проходит

Задача: «дан список из nn чисел, nn до 10510^5, значения от 0 до 1000; выведите, сколько значений встречается ровно один раз».

Ученик написал:

n = int(input())
a = list(map(int, input().split()))
count = 0

for x in a:
    if a.count(x) == 1:
        count += 1

print(count)

Решение верное и короткое, но получает превышение времени. Объясните, где спрятана вложенность, оцените количество действий и предложите решение, которое пройдёт.

Войдите, чтобы ответить.