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

Простые числа и решето

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

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

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

  • Выбирать между проверкой за корень и решетом по числу запросов
  • Строить решето Эратосфена и решето наименьших делителей
  • Раскладывать число на множители, не теряя остаток после цикла
  • Считать простые на далёком отрезке сегментным решетом

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

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

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

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

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

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

теория

Одно число или все сразу

Простое число — это число, у которого ровно два натуральных делителя: единица и оно само. Двойка простая, единица — нет: у неё всего один делитель.

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

def is_prime(n):
    if n < 2:
        return False
    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1
    return True

Для n=1012n = 10^{12} это миллион шагов — доли секунды. Отличный инструмент.

Пока не спросят миллион раз

А теперь задача: сколько простых чисел не превосходит миллиона?

Тем же способом это миллион проверок, каждая до тысячи шагов, — порядка 10910^9 действий. Минуты работы.

Вот это и есть развилка всего занятия. Один вопрос про большое число — проверка за корень. Много вопросов про числа поменьше — решето. Выбор определяется не тем, какой способ «лучше», а тем, сколько раз спрашивают.

теория

Решето Эратосфена

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

Заводим список отметок «простое» на все числа до nn. Идём по числам: если число ещё не вычеркнуто, оно простое — и мы вычёркиваем все его кратные.

flags = bytearray([1]) * (n + 1)
flags[0] = flags[1] = 0

i = 2
while i * i <= n:
    if flags[i]:
        for m in range(i * i, n + 1, i):
            flags[m] = 0
    i += 1

Две детали, которые легко пропустить

Почему вычёркивание начинается с i2i^2, а не с 2i2i? Потому что все меньшие кратные уже вычеркнуты. Число iki \cdot k при k<ik < i имеет множитель kk, меньший ii, — а по нему мы уже проходили раньше. Начинать с i2i^2 не оптимизация ради оптимизации: это ровно та граница, ниже которой работать бессмысленно.

Почему внешний цикл идёт только до корня? По той же причине. Если i>ni > \sqrt{n}, то первое вычёркиваемое число i2i^2 уже больше nn, и вычёркивать нечего.

Сколько это стоит

Вычёркиваний получается примерно nlnlnnn \ln \ln n — это почти линейно. Для n=107n = 10^7 решето строится за доли секунды.

Платим памятью: список на n+1n + 1 ячейку. Для 10710^7 в виде bytearray это десять мегабайт — нормально. Для 10910^9 уже гигабайт, и так делать нельзя.

тест

Проверка: откуда вычёркивать

Почему в решете вычёркивание кратных числа ii начинают с i2i^2, а не с 2i2i?

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

Разложение на множители

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

result = []
i = 2
while i * i <= n:
    while n % i == 0:
        result.append(i)
        n //= i
    i += 1
if n > 1:            # остаток — тоже простой множитель
    result.append(n)

Три вещи, которые тут происходят

Проверять простоту i не нужно. Когда цикл доходит до составного i, все его простые множители уже вычеркнуты делением, поэтому n % i == 0 для составного i просто не выполнится. Так что делить можно подряд, на 2, 3, 4, 5, — лишние проверки ничего не изменят.

Условие цикла смотрит на текущее n, а оно уменьшается. Это делает перебор быстрее: после деления на маленькие простые корень становится меньше.

Последняя строчка обязательна. После цикла n — это либо единица, либо простое число. Простое, потому что если бы остаток был составным, у него нашёлся бы делитель не больше его корня, а все такие делители мы уже перебрали. Забыть эту строчку — самая частая ошибка темы: для n=14n = 14 вывод получится 2 вместо 2 7.

Когда чисел много

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

spf = list(range(limit + 1))
i = 2
while i * i <= limit:
    if spf[i] == i:                     # i простое
        for m in range(i * i, limit + 1, i):
            if spf[m] == m:             # ещё не отмечено
                spf[m] = i
    i += 1

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

расчёт

Проверка: простые до тридцати

Сколько простых чисел не превосходит 30? Введите целое число.

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

Решето на далёком отрезке

Задача: сколько простых чисел лежит между 101210610^{12} - 10^6 и 101210^{12}?

Обычное решето до 101210^{12} построить нельзя — это терабайт памяти. Проверять каждое число по отдельности — миллион проверок по миллиону шагов, слишком долго. Но выход есть, и он опирается на ту же мысль про корень.

Составное число, не превосходящее RR, обязательно имеет простой делитель не больше R\sqrt{R}. Для R=1012R = 10^{12} это миллион.

Значит достаточно двух шагов:

  1. Построить обычное решето до R\sqrt{R} — это миллион ячеек, недорого.
  2. Завести отметки только на нужный отрезокRL+1R - L + 1 ячеек — и вычеркнуть в нём кратные каждого найденного простого.
flags = bytearray([1]) * (high - low + 1)
for p in base_primes:
    start = max(p * p, (low + p - 1) // p * p)   # первое кратное p не меньше low
    for m in range(start, high + 1, p):
        flags[m - low] = 0                       # сдвиг индексов!

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

Два места, где ошибаются: сдвиг индексов (в списке ячейка с номером m - low, а не m) и случай L=1L = 1 — единицу надо вычеркнуть вручную, её ни одно простое не вычеркнет.

теория

Типичные ошибки

Потерянный остаток при разложении. Разобрано выше. Проверяйте на 14, на простом числе и на произведении двух больших простых.

Единица и ноль. Не простые и не составные. В решете их надо вычеркнуть отдельной строкой — сам алгоритм этого не сделает.

Список на единицу короче. bytearray(n) даёт ячейки с номерами от 0 до n1n - 1, а нам нужно и само nn. Признак: неверный ответ ровно тогда, когда nn простое.

Решето вместо проверки и наоборот. Решето до 101210^{12} не построить, а проверять миллион чисел по отдельности слишком долго. Прежде чем писать, прикиньте: сколько раз спросят и насколько большие числа.

Забытый сдвиг в сегментном решете. Обращение flags[m] вместо flags[m - low] либо упадёт, либо молча испортит ответ.

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

  • n=1n = 1 и n=2n = 2 — крайние случаи всех задач темы;
  • nn простое и nn — степень двойки;
  • произведение двух больших простых: 999 962 000 357 = 999 983 · 999 979 — здесь перебору до корня придётся дойти почти до конца;
  • в задачах на отрезок — L=1L = 1 и отрезок из одного числа.
теория

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

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

Перед каждой задачей отвечайте себе на один вопрос: спрашивают про одно число или про все сразу? От ответа зависит, что писать — проверку за корень или решето.

В задачах со звёздочкой числа доходят до триллиона, а отрезок короткий. Это прямая подсказка на сегментное решето.

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

Разложение без хвоста

Задача: «дано nn от 1 до 101210^{12}; выведите его простые множители по возрастанию, с повторами».

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

n = int(input())

factors = []
i = 2
while i * i <= n:
    while n % i == 0:
        factors.append(i)
        n //= i
    i += 1

print(*factors)

На числах 12, 36 и 1024 ответ верный, по времени решение проходит.

Найдите вход, на котором оно ошибается, объясните причину и предложите исправление.

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