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

Оценка сложности: почему решение не проходит

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

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

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

  • Прикидывать количество шагов до написания кода
  • Читать ожидаемую сложность по ограничениям задачи
  • Различать O(n), O(n log n) и O(n²) на практике
  • Замечать скрытые проходы внутри коротких записей

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

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

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

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

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

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

теория

Сколько успевает компьютер

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

Ориентир простой. За одну секунду Python успевает порядка 10710^7 простых операций. Компилируемые языки — примерно в тридцать раз больше, около 31083 \cdot 10^8.

Простая операция — это одно сравнение, одно сложение, одно обращение к элементу списка. Не вызов метода, который сам пробегает по данным, — об этом отдельно.

Отсюда получается таблица, которую полезно помнить наизусть:

сколько шагов Python вывод
10610^6 0,1 с свободно
10710^7 1 с впритык, но обычно проходит
10810^8 10 с не проходит
10910^9 и больше минуты и часы даже не пробуйте

Обратите внимание: разница между «проходит» и «не проходит» — это не разница в аккуратности кода. Это разница в количестве шагов, то есть в самом подходе.

теория

Как считают количество шагов

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

Записывают это буквой OO и оставляют только главное: константы и младшие слагаемые отбрасывают.

запись смысл пример
O(1)O(1) не зависит от данных формула, обращение по номеру
O(logn)O(\log n) делим пополам быстрое возведение в степень
O(n)O(n) один проход сумма, максимум, словарь частот
O(nlogn)O(n \log n) проход плюс сортировка sort, sorted
O(n2)O(n^2) все пары вложенные циклы по одному списку
O(n3)O(n^3) все тройки три вложенных цикла

Почему отбрасывают константы: разница между 2n2n и 5n5n — это разница в разы, а между nn и n2n^2 при ста тысячах — в сто тысяч раз. Второе решает судьбу решения, первое почти никогда.

Отдельно про O(nlogn)O(n \log n): множитель logn\log n — это примерно 17 при n=100000n = 100\,000 и 20 при миллионе. То есть сортировка дороже одного прохода в двадцать раз, а квадрат — в сто тысяч. Между ними пропасть, и сортировки бояться не нужно.

тест

Проверка: пройдёт ли

В задаче nn до 10510^5, и решение перебирает все пары элементов.

Что будет?

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

Ограничения — это подсказка

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

ограничение что должно получиться
n1018n \le 10^{18} формула или O(logn)O(\log n)
n106n \le 10^6 один проход, O(n)O(n)
n2105n \le 2 \cdot 10^5 O(n)O(n) или O(nlogn)O(n \log n)
n5000n \le 5000 можно O(n2)O(n^2)
n500n \le 500 можно O(n3)O(n^3)
n20n \le 20 перебор всех вариантов

Читать эту таблицу нужно до того, как писать код. Увидев nn до двухсот тысяч, вы сразу знаете: перебора пар не будет, нужен словарь, множество или сортировка.

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

Маленькое ограничение — это разрешение, а не ловушка. Оно говорит: «тут можно в лоб».

теория

Почему Python требует аккуратности

Два обстоятельства, из-за которых оценка «на глаз» в Python обманывает.

Первое: цикл дорог, а встроенное дёшево. Метод sort, функция sum, проверка in для множества написаны на C и работают в десятки раз быстрее того же самого, набранного циклом. Поэтому sum(a) предпочтительнее ручного накопления — не ради красоты, а ради скорости.

Второе: стоимость строки не видна. Вот примеры, где одна короткая запись стоит целого прохода:

запись кажется на самом деле
x in a для списка одно действие O(n)O(n)
a.count(x) одно действие O(n)O(n)
a.index(x) одно действие O(n)O(n)
a[1:] в цикле одно действие O(n)O(n) на каждый срез
s += ch для строки одно действие создание новой строки

Каждая из них внутри цикла превращает O(n)O(n) в O(n2)O(n^2) — незаметно, потому что вложенности в тексте нет.

Проверка простая: если внутри цикла стоит что-то, что само проходит по данным, сложность умножается. Именно так решение из шести аккуратных строк получает превышение времени.

расчёт

Проверка: во сколько раз

Решение работало за O(n)O(n) и укладывалось в секунду при n=106n = 10^6.

Во сколько примерно раз дольше будет работать решение за O(n2)O(n^2) при том же nn? Введите степень десяти — то есть если ответ «в миллион раз», введите 6.

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

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

Оценка по виду кода, а не по смыслу. Шесть строк могут работать дольше, чем тридцать. Считать надо шаги, а не строки.

Забытая стоимость встроенного. count, index, in по списку, срезы — всё это проходы. Внутри цикла они дают квадрат.

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

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

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

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

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

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

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

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

Оцените три решения

Для задачи «дан список из nn чисел, найдите количество пар элементов с равными значениями» написали три решения. Ограничение: nn до 21052 \cdot 10^5.

Первое

count = 0
for i in range(len(a)):
    for j in range(i + 1, len(a)):
        if a[i] == a[j]:
            count += 1

Второе

count = 0
for value in set(a):
    c = a.count(value)
    count += c * (c - 1) // 2

Третье

counts = {}
for value in a:
    counts[value] = counts.get(value, 0) + 1
count = 0
for key in counts:
    c = counts[key]
    count += c * (c - 1) // 2

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

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