Статья Автор: Лебедев Дмитрий Алексеевич

Проблема K. Количество простых чисел на отрезке

Проблема K. Количество простых чисел на отрезке.

Дан отрезок [A;B]. Надо определить сколько простых чисел принадлежит отрезку.
Входные умения: проверка на простоту с помощью "поиска минимального делителя числа" (программа min_del)
Задание:
написать функцию count_prime(A, B), которая возвращает число простых чисел на отрезке
Способ 1 - перебрать все числа отрезка, для каждого определить "простое оно или нет"


Можно убедиться, что программа работает для A,B таких, что B-A не очень велико.
Для альтернативного способа можно предложить использование метода "решето Эратосфена", но не объяснять как его реализовать эффективно.
Возьмет описание из интернета:

Для нахождения всех простых чисел не больше заданного числа n, следуя методу Эратосфена, нужно выполнить следующие шаги:

  1. Выписать подряд все целые числа от двух до n (2, 3, 4, ..., n).
  2. Пусть переменная p изначально равна двум — первому простому числу.
  3. Зачеркнуть в списке числа от 2p до n, считая шагами по p (это будут числа, кратные p: 2p, 3p, 4p, ...).
  4. Найти первое незачёркнутое число в списке, большее чем p, и присвоить значению переменной p это число.
  5. Повторять шаги 3 и 4, пока возможно.
Возможно ученик его реализует "дословно". Приведем примерное решение. 
Программа sieve0(A,B) получает границы полуинтервала [A, B) и 
возвращает список из B элементов, таких что: B[p] = 1 если p - простое и 0 -в противном случае   


Можно убедиться, что на отрезке [1,1000007] решето уже дает большой выигрыш во времени.
Теперь можно приступить к этапу "повышение эффективности программ".
Для этого ещё раз "понять":
  • что 4 мы тоже "пытаемся вычеркивать". Значит надо добавить проверку d (делителя) на простоту
    то есть добавить условие M[d] == 1
  • а можно ли начинать "просеивание" не с 2d? (ведь, если d > 2, то это число уже "вычеркнуто")
    должны понять, что можно с d*
Внесем изменения и проверим прирост "эффективности", сравнив решения sieve0 и sieve1
(сравниваем только развертывание решета, без подсчета количества простых)


Получим явное повышение эффективности (это можно замерить на отрозке от 1 до 10_000_000).
Добавим еще одно "логическое усовершенствование"
А почему вначале список M весь заполняется 1? Может быть исключить четные сразу
  


Теперь "технические" тонкости или "фишки Python"
Применим следующий прием: "замене изменения элементов списка в цикле" на  "замену среза "
for i in range(d * d,  B, d) : M[i] = 0
заменим на M[d*d:B:d ] = [0]*L (L = len(M[d*d:B:)
Пока не будем вычислять L алгеброически
Сравним результаты на отрезке [10_000_000; 20_000_000]


Итак, 1 фокус получился - результат увеличился в два раза.
Но, почему мы находили  значение L как длину среза?
Можно ли это сделать через простое вычисление и  ускорить работу программы?
Несложно понять, что L - это количество чисел полуинтервала [d2; B) кратных d
N([d2; B)) =N([0; B)) - N([0;d2) , где N([X;Y)) - количество чисел полуинтервала [X;Y) кратных d
Нетрудно понять, что N([0;d2) = d (это числа 0, d, 2d, ..., (d-1)d)
Также, можно проверить, что N([0; B)) равно B//i с округлением в большую сторону или
N([0; B)) = (B-1)//d + 1 = (B + d -1)//d = -(-B//d) (понять последнее сложнее, но для Python это верно)
Заменим L на -(-B // d + d) и создадим новую версию программы
 


Получили еще "хороший" прирост эффективности. 
На отрезке от 10_000_000 до 20_000_000 он состовил более 25%
На этом пока остановимся и подведем итоги:
  1. Эффективным методом получения/подсчёта чисел на отрезке (большом) может быть может быть метод решета Эратосфена
  2. "Решето" можно изначально сформировать в "предсформированном" виде (здесь есть место для творчества) 
  3. Для "просеивания", вместо цикла, эффективнее использовать срезы
Ниже приведен вариант полученной программы, которая может за реальное время подсчитать количество простых чисел на отрезке [1; 100_000_000]

Печать