Статья Автор: Деникина Н.В., Деникин А.В.

Четыре универсальные функции дял задания 25

Четыре универсальные функции
для задания 25

Весь арсенал задачи 25 укладывается в четыре короткие функции: divs, is_prime, sieve, factor. Выучив их один раз, на экзамене вы просто комбинируете их под конкретное условие — не переписываете логику с нуля.

В этом параграфе
  1. Почему math.isqrt лучше int(n**0.5) — пример, где второй ошибается
  2. Функция 1 — divs(n): все делители за O(√n)
  3. Функция 2 — is_prime(n): проверка простоты
  4. Функция 3 — sieve(N): решето Эратосфена на bytearray
  5. Функция 4 — factor(n): факторизация в простые множители
  6. Шпаргалка: какие функции использовать для каждого типа задачи 25

Почему isqrt, а не int(n**0.5) фундамент всего параграфа

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

Способ 1 — через float: int(n**0.5). Сначала Python возводит n в степень 0.5 (получается число с плавающей точкой float), затем откусывает дробную часть через int.

Способ 2 — через целочисленную функцию: isqrt(n) из модуля math. Эта функция работает без вычислений с плавающей точкой: она сразу ищет максимальное целое k, для которого k² ≤ n, используя целочисленный алгоритм.

Что делает isqrtmath.isqrt(n) возвращает наибольшее целое k, такое что k*k <= n. Это целочисленный квадратный корень — без float, без потери точности, без округлений.

В чём разница на практике? Тип float в Python (это стандарт IEEE 754 двойной точности) хранит около 15–16 значащих десятичных цифр. Для чисел больше 10¹⁵ округления уже дают о себе знать. Возьмём конкретное число:

Контрпример: int(n**0.5) ошибаетсяn = 999 999 999 999 999 999 # почти 10^18 int(n**0.5)1 000 000 000 # float округлил вверх isqrt(n)999 999 999 # правильно проверка: 999 999 999² = 999 999 998 000 000 001 ≤ n ✓ 1 000 000 000² = 1 000 000 000 000 000 000 > n ✗

Разница в единицу — казалось бы мелочь, но если цикл идёт for d in range(2, int(n**0.5) + 1), лишняя итерация может или пропустить корневой делитель, или наоборот заглянуть за границу — оба варианта дают неверный ответ.

Второе преимущество isqrtскорость. Целочисленный алгоритм в C-реализации быстрее, чем вычисление **0.5 через плавающую точку плюс приведение к int. На миллионе вызовов разница ощутима.

Третье — корректная граница. При переборе делителей нужен диапазон range(1, isqrt(n) + 1): символ +1 стоит потому, что range не включает правую границу, а нам нужно дойти до самого isqrt(n).

ПравилоВезде, где нужна граница перебора до корня, пишем isqrt(n) + 1, не int(n**0.5) + 1. Один импорт — и гарантия корректности на любых числах.

Функция 1 — delitels все делители числа

Самая используемая функция в задании 25. Возвращает отсортированный список всех натуральных делителей числа n — от 1 до самого n.

Принцип работы разобран в §01: делители ходят парами, один из пары всегда не превосходит √n. Значит достаточно пробежать d от 1 до isqrt(n) и на каждом совпадении добавить оба делителя. Чтобы не получить дубликат при n = k² (когда d = n // d), используем set — он автоматически отбросит повтор.

Python 7 строк
from math import isqrt

def divs(n):
    D = set()
    for d in range(1, isqrt(n) + 1):
        if n % d == 0:
            D.add(d)
            D.add(n // d)
    return sorted(D)

Что возвращает и как использовать:

Примеры вызововdivs(12) → [1, 2, 3, 4, 6, 12] divs(36) → [1, 2, 3, 4, 6, 9, 12, 18, 36] # 6 не задвоилось divs(17) → [1, 17] # простое divs(1) → [1] кол-во делителей: len(divs(n)) нетривиальные делители: divs(n)[1:-1] # отрезаем 1 и n мин. нетривиальный: divs(n)[1] # второй элемент макс. нетривиальный: divs(n)[-2] # предпоследний

Срез divs(n)[1:-1] — чистый и короткий способ получить нетривиальные делители (без 1 и без самого n). Для простых n возвращает пустой список — удобно для проверок.

Функция 2 — is_prime проверка простоты

Проверяет, является ли число n простым. Возвращает True или False. В задании 25 используется всюду, где фигурируют «простые множители» — блоки §03, §04.

Логика такая: число простое, если у него нет делителей от 2 до √n. Как только нашли хотя бы один — сразу возвращаем False. Если дошли до конца цикла без совпадений — значит число простое, возвращаем True.

Оптимизация: после проверки делимости на 2 шагаем по нечётным числам (с шагом 2). Это вдвое ускоряет функцию без потери корректности — если n не делится на 2, оно точно не делится на 4, 6, 8...

Python 8 строк
from math import isqrt

def is_prime(n):
    if n < 2: return False
    if n < 4: return True       # 2 и 3 — простые
    if n % 2 == 0: return False  # чётные (кроме 2) — составные
    for d in range(3, isqrt(n) + 1, 2):
        if n % d == 0: return False
    return True
Примеры вызововis_prime(2) → True is_prime(17) → True is_prime(100) → False is_prime(1) → False # 1 не считается простым is_prime(0) → False is_prime(9901) → True # четырёхзначное простое

Для диапазона ЕГЭ (n до 10¹⁰) функция отрабатывает за ~100 000 итераций в худшем случае — миллисекунды. Если понадобится проверять простоту тысяч чисел в одном цикле — лучше использовать решето (Функция 3).

Функция 3 — sieve решето Эратосфена

Когда задача требует не проверить одно число, а получить список всех простых до некоторой границы N — решето Эратосфена на порядки быстрее, чем вызывать is_prime для каждого числа по отдельности.

Идея: создаём массив флагов длины N+1, где sieve[i] означает «число i — простое?». Изначально все единицы, кроме sieve[0] и sieve[1]. Затем для каждого простого p зачёркиваем все его кратные: 2p, 3p, 4p, .... В конце единицы в массиве стоят только на простых позициях.

Для хранения флагов используем bytearrayвстроенный тип Python (как list или str, импорты не нужны, на ЕГЭ абсолютно легален). bytearray хранит байты, поэтому занимает в 8 раз меньше памяти, чем list из целых чисел. Для N = 10⁷ это разница между 10 МБ и 80 МБ — критично на ЕГЭ, где ограничения на память жёсткие.

bytearrayВстроенный тип Python, доступен везде без импортов. Как list, но элементы — только байты (0–255). Идеален для хранения флагов (0/1) и зачёркивания через срезы.
Python 7 строк
from math import isqrt

def sieve(N):
    a = bytearray([1]) * (N + 1)
    a[0] = a[1] = 0
    for p in range(2, isqrt(N) + 1):
        if a[p]:
            a[p*p :: p] = bytearray(len(a[p*p :: p]))
    return a

Главная тонкость: начинаем зачёркивание с p*p, а не с 2p. Все числа 2p, 3p, ..., (p-1)·p уже были зачёркнуты ранее — при обработке простых меньше p. Первое «новое» кратное, которое ещё не тронуто, — это p·p. Поэтому внешний цикл идёт только до isqrt(N): если p > isqrt(N), то p² > N и вычёркивать нечего.

Строка a[p*p :: p] = bytearray(len(a[p*p :: p])) обнуляет сразу все кратные p, начиная с . Эта идиома выглядит непривычно — её подробный разбор в следующей секции.

Использование результатаa = sieve(100) a[17] → 1 # 17 простое a[18] → 0 # 18 составное a[1] → 0 a[2] → 1 все простые до 100: [i for i in range(len(a)) if a[i]] → [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, ...]

На каких размерах N имеет смысл звать sieve? Для N ≤ 10⁷ — точно, отработает за пару секунд и съест ~10 МБ. Для N = 10⁸ — уже на грани, 100 МБ может не хватить. Если задача просит простые в диапазоне до 10⁹, решето не подходит — лучше использовать is_prime для отдельных чисел.

Как работает массовое зачёркивание разбор самой хитрой строки

Самая непонятная строка в sieve — это a[p*p :: p] = bytearray(len(a[p*p :: p])). За одну операцию она обнуляет все элементы массива с индексами p², p²+p, p²+2p, … — то есть все кратные p, начиная с . Разберём по кусочкам.

Возьмём крошечный пример для наглядности: a = bytearray([1] * 20) — массив из 20 единичек с индексами от 0 до 19. И пусть p = 3. Тогда p*p = 9, и мы хотим обнулить a[9], a[12], a[15], a[18] — все кратные 3, начиная с 9.

Шаг 1. Срез a[p*p :: p]a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 срез a[9 :: 3] берёт элементы с индексами 9, 12, 15, 18 # синтаксис: a[start :: step] # start = 9 (откуда начинать) # step = 3 (с каким шагом идти) # конец = до конца массива результат: [1, 1, 1, 1] # длина 4
Шаг 2. len(a[p*p :: p])len(a[9 :: 3])4 столько элементов в этом срезе, столько нулей нам понадобится для замены
Шаг 3. bytearray(4) — что это создаёт?bytearray(4)bytearray(b'\x00\x00\x00\x00') # массив из 4 нулей ВАЖНО: конструктор bytearray работает по-разному: bytearray([1, 0, 1])массив [1, 0, 1] (из списка) bytearray(b'abc')массив [97, 98, 99] (из bytes) bytearray(4)массив [0, 0, 0, 0] (целое → нули!) Именно эта последняя форма используется в sieve: получить массив нужной длины, заполненный нулями
Шаг 4. a[9 :: 3] = bytearray(4)до присваивания: a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] ^ ^ ^ ^ 9 12 15 18 после присваивания: a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1] ^ ^ ^ ^ Python прошёл по четырём позициям среза (9, 12, 15, 18) и в каждую записал соответствующий элемент из bytearray(4) — ноль. За одну операцию обнулены ровно все нужные позиции.

В применении к решету Эратосфена: если p = 3 и N = 20, то после этой строки все числа 9, 12, 15, 18 помечены как составные. Внешний цикл перейдёт к следующему простому (p = 5), и снова одной строкой обнулит a[25], a[30], ... — и так далее.

КлючСинтаксис a[start :: step] = bytearray(len(...)) — Python-идиома для «обнулить все позиции среза одним махом». Главная выгода — это делается на уровне C, а не Python-циклом.

Почему не написать проще — через обычный цикл? Сравним два варианта:

Python · сравнение цикл vs срез
# Вариант A — через Python-цикл (работает, но медленно)
for k in range(p*p, N + 1, p):
    a[k] = 0

# Вариант B — через срез (быстро, идиома)
a[p*p :: p] = bytearray(len(a[p*p :: p]))

Оба варианта дают абсолютно одинаковый результат. Но вариант B работает в 10–20 раз быстрее. Причина проста.

В варианте A на каждую итерацию Python выполняет десятки внутренних операций: проверка условия цикла, вычисление следующего k, вызов a.__setitem__, инкремент счётчика. Всё это — шаги интерпретатора. Для N = 10⁷ и простого p = 2 нужно пройти 5 миллионов итераций. Добавьте сюда все остальные простые — получите десятки миллионов медленных операций.

В варианте B присваивание в срез обрабатывается одной функцией на уровне C: она напрямую пишет нули в память массива, без возврата в интерпретатор Python между элементами. Для того же N = 10⁷ вся операция занимает десятые доли секунды.

Практический замер: sieve до N = 10 000 000Вариант A (цикл): ~1.6 секунды Вариант B (срез): ~0.1 секунды Разница в ~16 раз — на одной строке кода На границе таймаута ЕГЭ (обычно 1-5 сек) это может быть разница между «задача уложилась» и «не успели».

Почему вообще срез-идиома эффективна именно для bytearray, а не для list? Потому что bytearray хранит байты непрерывно в памяти (как массив в C), и CPython умеет копировать в него байты блоками через memmove. У list внутри — массив указателей на Python-объекты, и массовое присваивание там медленнее.

Итого: bytearray + срез-идиома — это не просто «короче написать», а принципиально более быстрый способ работы с массивом флагов. Для задания 25 запоминаем именно эту пару.

Функция 4 — factor факторизация

Раскладывает n на простые множители с учётом кратности. Возвращает список простых, где каждое простое повторяется столько раз, сколько оно входит в разложение.

Нужна в задачах, где требуется узнать структуру числа: «состоит из двух различных простых», «является степенью простого», «имеет три простых множителя». В §03 мы делали факторизацию через цикл с первым делителем — функция factor делает то же самое в общем виде.

Алгоритм: идём d от 2 и «откусываем» от n все вхождения d (в цикле while n % d == 0, добавляя d в список и деля n на d). Когда d*d > n, цикл заканчивается. Если после цикла n > 1 — остаток сам является простым и добавляется в список.

Python 10 строк
def factor(n):
    f = []
    d = 2
    while d * d <= n:
        while n % d == 0:
            f.append(d)
            n //= d
        d += 1
    if n > 1:
        f.append(n)
    return f
Примеры вызововfactor(12) → [2, 2, 3] # 12 = 2² · 3 factor(60) → [2, 2, 3, 5] # 60 = 2² · 3 · 5 factor(17) → [17] # простое factor(100) → [2, 2, 5, 5] # 100 = 2² · 5² factor(1) → [] # пустой список число различных простых: len(set(factor(n))) является ли p · q (различные): len(factor(n)) == 2 and len(set(factor(n))) == 2 является ли p² · q: sorted(list(Counter(factor(n)).values())) == [1, 2]

Обратите внимание: factor возвращает множители с повторами. Чтобы получить уникальные простые — применяем set(factor(n)). Чтобы получить разложение в виде «простое → степень» — можно использовать collections.Counter, но для школьной задачи обычно достаточно factor + set.

Шпаргалка какую функцию использовать для каждого типа задачи

Эта таблица — главное, ради чего написан параграф. Запоминаем один раз — применяем на любой задаче 25.

Тип задачи Что использовать Как
Ровно K делителей divs len(divs(n)) == K + 2 (если «не считая 1 и n»)
min + max нетривиальных делителей divs d = divs(n); M = d[1] + d[-2]
Сумма / произведение делителей divs sum(divs(n)[1:-1]), prod(divs(n)[1:-1])
Чётные / нечётные делители divs [d for d in divs(n) if d % 2 == 0]
Простое число is_prime is_prime(n)
N = p · q (два различных простых) factor f = factor(n); len(f) == 2 and f[0] != f[1]
N — произведение K различных простых factor f = factor(n); len(f) == K == len(set(f))
Число свободно от квадратов factor f = factor(n); len(f) == len(set(f))
Простые множители с условием на цифры factor all('5' in str(p) for p in set(factor(n)))
Массовая генерация простых до N sieve a = sieve(N), далее a[k]
Маска + делимость (из §05) ни одна из четырёх fnmatch + шаг по кратным K
ИтогЧетыре функции закрывают ~95% задачи 25. Остальное — маски (fnmatch) и редкие типы. Выучите эти восемь-десять шаблонов — и задача 25 становится не проблемой, а разминкой на 3 минуты.

Полный комплект для вставки в начало решения копируется целиком

Храните эти 30 строк в готовом виде. На экзамене вставляете в начало программы, дальше используете любую комбинацию функций под условие конкретной задачи.

Python · toolkit 30 строк
from math import isqrt

def divs(n):
    D = set()
    for d in range(1, isqrt(n) + 1):
        if n % d == 0:
            D.add(d)
            D.add(n // d)
    return sorted(D)

def is_prime(n):
    if n < 2: return False
    if n < 4: return True
    if n % 2 == 0: return False
    for d in range(3, isqrt(n) + 1, 2):
        if n % d == 0: return False
    return True

def sieve(N):
    a = bytearray([1]) * (N + 1)
    a[0] = a[1] = 0
    for p in range(2, isqrt(N) + 1):
        if a[p]:
            a[p*p :: p] = bytearray(len(a[p*p :: p]))
    return a

def factor(n):
    f, d = [], 2
    while d * d <= n:
        while n % d == 0:
            f.append(d)
            n //= d
        d += 1
    if n > 1: f.append(n)
    return f
Печать