Об античном ученом Эратосфене Киренском, которому приписывают наш метод, википедия говорит следующее [1]:
«Эратосфен Киренский (др.-греч. ἘρατοσθένηςὁΚυρηναῖος; 276 годдон. э.—194 годдон. э.) —греческий математик, астроном, географ, филолог и поэт. Ученик Каллимаха, с 235 г. дон. э. —глава Александрийской библиотеки. Первый известный учёный, вычисливший размеры Земли.»
Обычно метод Эратосфена описывают при помощи картинки, где изображено некоторое количество первых чисел натурального ряда (единицу, как и договорились, мы сразу выбросим из рассмотрения):
![Первые 50 натуральных чисел Первые 50 натуральных чисел](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/91d/0ac/308/91d0ac308803bbdde12dba22a27b3330.jpg)
Идея в том, что мы берем подряд числа из натурального ряда. Для каждого взятого числа k добавляем это же число k и полученный результат вычеркиваем. Потом снова добавляем и снова вычеркиваем. Делаем так, пока не дойдем до конца интервала. Почему мы вычеркиваем эти числа? Потому что это каждое такое число кратно k, оно заведомо составное, а не простое.
Возьмем для начала двойку. Кстати, а саму двойку надо вычеркивать или нет? Ответ, с одной стороны, очевидный, с другой стороны – какой-то неожиданный. Этот ответ звучит так: если очередное число при проходе по натуральному ряду еще не вычеркнуто, то и не надо его вычеркивать, оно «хорошее». Это как в сказке «Волшебная лампа Алладина» султан в сердцах постановил для своей непослушной дочери: «Отдам тебя замуж за первого встречного!». Вот мы начали перебирать наш ряд, взяли двойку. Мы ее встретили в первый раз, она еще не вычеркнута – значит, подходит! Это простое число – и мы его не вычеркиваем.
Но мы идем дальше. К двойке прибавляем само это число 2+2. Получилось 4. Надо ли здесь задавать себе вопрос: в первый раз или не в первый мы встретили это число? Нет, не надо. Потому что мы работаем с двойкой! Получившееся число 4 вычеркиваем.
Кстати, как долго нам надо идти вперед? Это зависит от точной постановки задачи, а мы пока этого не сделали, и подходили к задаче по-простецки - находили простые числа. Но мы не уточняли, сколько именно простых чисел мы хотим найти. В самом деле – сколько?
Поиск простых чисел на заданном интервале
Наверное, имеет смысл искать N первых простых чисел. Но это не очень простая постановка задачи – вы это скоро увидите. Давайте для начала рассмотрим более простую задачу: найти все простые числа, меньше или равные заданного числа M.
Пусть это M равно 50. Значит, надо добавлять нашу двойку, пока не дойдем до 50.
Начав с 2, и добавляя по 2, мы получим следующую картину (красным выделены вычеркнутые числа):
![Вычеркнули от 2 по +2 Вычеркнули от 2 по +2](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/637/5df/8d7/6375df8d7b940a72e9d99b4e50720b9e.jpg)
Теперь давайте разбираться с числом 3. Саму тройку мы оставляем по принципу «первого встречного», а дальше зачеркиваем 6, 9, 12, и так далее:
![Вычеркнули от 3 по +3 Вычеркнули от 3 по +3](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/695/432/4b4/6954324b474bb4e5b7317160247bd9c0.jpg)
Нетрудно видеть, что какие-то числа нам пришлось вычеркивать по второму разу, начиная с той же шестерки – ведь мы уже вычеркнули 6, когда работали с числом 2. Есть ли в этом что-то нехорошее? Если рассматривать эту работу с вычислительной точки зрения, то как будто бы это нехорошо. Вообще любая повторная работа, которую можно избежать – это нехорошо. Потому что мы тратим вычислительное время. Но в данном случае лучше все же закрыть глаза на эту избыточность. Потому что иначе нам пришлось бы как-то проверять, а не вычеркивали ли мы это число раньше? А это, само по себе, тоже лишняя вычислительная работа.
С числом 3 разобрались. Надо ли нам разбираться с числом 4? Нет, принцип «первого встречного» говорит, что не надо – четверку мы уже встречали раньше, когда вычеркивали, начиная с двойки.
Тогда идем дальше и смотрим на число 5. Пятерка – как раз «первый встречный», мы еще ее «не видели». Значит, оставляем 5 в «хороших числах», а вот 10, 15, 20, 25 и так далее до 50 (включительно) – вычеркиваем:
![Вычеркнули от 5 по +5 Вычеркнули от 5 по +5](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/9d0/304/aaf/9d0304aaf767e99f26f98f25aa6a5d8b.jpg)
Число 6 снова пропускаем, зато семерка – «первый встречный», и мы вычеркиваем 21 (14 уже вычеркнули), 28, 35, 42, 49:
![Вычеркнули от 7 по +7 Вычеркнули от 7 по +7](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/244/113/563/244113563e7bfc8528516b1af9516162.jpg)
Как долго следует продолжать этот процесс? В самом начале мы сказали, что надо перебирать все числа в заданном интервале [2; M]. Но нетрудно догадаться, что на самом деле достаточно остановиться на числе M/2 (или его целой части для нечетных M). Ведь когда мы перейдем к следующему за ним числу K = [M/2] + 1, то число K + K заведомо окажется вне диапазона [2; M].
В любом случае, когда мы закончим перебирать все числа в интервале [2; M] (или только первую половину этого интервала), мы получим, что больше нам вычеркивать нечего. Это значит, что числа, оставшиеся незакчеркнуыми – и есть простые:
![Вычеркнули от 7 по +7 Вычеркнули от 7 по +7](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/84e/343/19b/84e34319b08b7874065f1a13655697a0.jpg)
Всего мы нашли 16 простых чисел на нашем интервале, причем заранее мы не могли знать, сколько именно мы найдем. В этом и состоит главная прелесть и загадка простых чисел: не существует простого понимания того, насколько плотно простые числа расположены на числовой оси. Вспомним, какую постановку задачи мы сами для себя выбрали – найти все простые числа на заданном отрезке, – то есть, не превосходящие некоторого числа M.
Это, конечно, здорово, но согласитесь – куда интереснее решить задачу о нахождении конкретного количества N простых чисел. На самом деле, с точки зрения некоторого вымышленного, но типового «заказчика», куда как важнее получить конкретное количество первых простых чисел, чем просто все, что можно найти на заданном интервале.
Давайте для начала запрограммируем на Python первую задачу – для заданного интервала, а потом станем думать, как это трансформировать в решение второй задачи.
# указываем верхнюю границу поиска
maxNum = 100
# создаем список вообще всех целых чисел от 0 до maxNum включительно
nums = list(range(maxNum + 1))
# мы будем рассматривать ненулевые числа; единицу тоже занулим сразу,
# потому что мы договорились, что это число не является простым
nums[1] = 0
# сюда будем складывать «женихов» - «первых встречных», т.е. найденные
# простые числа
primes = []
# внешний цикл – проход по заданному интервалу
for i in nums :
# рассматриваем только ненулевые числа; 0 – значит число негодное,
# мы его вычеркнули (не «первый встречный»)
if i > 1:
# Внутренний цикл: для каждого числа i проходим «прыжками» с шагом i
for j in range(i + i, len(nums), i):
nums[j] = 0
# не забываем сохранить само число
primes.append(i)
print(len(primes))
В результате чего убедимся, что на отрезке [1; 100] всего 25 простых чисел. А теперь представим, что нашему заказчику совершенно все равно, какой интервал мы рассматриваем. Ему важно, чтобы мы нашли ему 100 первых простых чисел. А уж на каком интервале они лежат – как получится. Как нам нужно действовать?
Поиск заданного количества простых чисел
Наверно, самый простой ответ будет такой: давайте возьмем поисковый интервал «на глазок». Можно предположить, что это должен быть достаточно большой интервал. Например, выше мы убедились, безо всякого программирования, что на отрезке [1; 50] есть 16 простых чисел. Значит, если наш заказчик хочет найти 100 простых чисел, можно взять поисковый интервал примерно от 1 до 300.
Почему бы и нет. Но на самом деле, частота, с которой на числовой оси встречаются простые числа, убывает с ростом максимального номера простого числа. Или, другими словами, необходимый поисковый интервал растет быстрее (это называется «нелинейно»), чем количество простых чисел, расположенных на этом интервале.
Это очень неприятная для нас новость. Если бы плотность простых чисел на числовой оси была хотя бы примерно постоянной, и тем более, если бы она уменьшалась с ростом номера простого числа, то мы могли бы поступить именно предложенным способом. Но чем дальше в лес, тем реже встречаются простые числа. А это значит, что чем больше простых чисел нам надо найти, тем сильнее нам надо «раздвигать» поисковый интервал.
Кстати, почему это так? Это очень интересный вопрос, но им надо подробно заниматься не здесь, а в теории чисел. Но на самом деле, интуитивно, это и так ясно. Ведь чем больше число, тем больше шансов, что у него окажутся делители, большие единицы, но меньшие его самого.
Можем ли мы хотя бы оценить, насколько реже – в каком-то количественном исчислении - будут попадаться простые числа по мере продвижения по числовой оси? Наверное, можно. Но, опять же, сейчас это не наша тема. Пусть этим занимаются числовые теоретики.
Как же нам поступить? Предыдущее решение было слишком ненадежным, мы это уже поняли. Следующее предложение: давайте постепенно наращивать поисковый интервал. Скажем, мы обработали интервал [1; 50], нашли 16 простых чисел, а заказчику надо 100 – значит, у нас явно «недолет». Давайте искать заново, скажем, на удвоенном интервале [1; 100] (двойка - это хороший коэффициент, поверьте). Если снова найдем меньше чисел, чем надо – еще раз удвоим: будем искать на отрезке [1:200], и так далее. Пока не найдем столько простых чисел, сколько у нас просят. А если найдем больше, чем нужно – это не беда: больше – не меньше, лишние всегда можно отбросить.
В каком-то смысле, на этом месте можно было бы остановиться и считать задачу решенной. Мы нашли принципиальный способ находить требуемое количество первых простых чисел. Но, к сожалению, с практической точки зрения, этот способ очень плохой. Все дело в том, что нам придется многократно повторять одну и ту же работу. Представьте, что мы уже проделали все нужные вычисления на отрезке [1; M] и не нашли требуемое количество N простых чисел. Мы говорим: не беда! – и начинаем работу заново на отрезке [1; 2M]. И нам неизбежно придется пройти сначала по отрезку [1; M], но ведь мы это уже делали! Получается, мы сделаем эту работу повторно.
И это совсем не безобидная избыточность. Попробуем как-то оценить объем вычислительной работы. Пусть на начальном интервале [1: M] мы проделали условно m вычислительных операций. Сколько операций мы выполним при расширении поискового интервала? Давайте для оценки считать, что проработка любого интервала длины M требует одно и то же – m – количество вычислений. На самом деле это не так – выше мы уже поняли, что «чем дальше в лес», тем реже простые числа. Но у нас нет на руках никакого точного «закона», который говорил бы – насколько реже. Значит, нам ничего не остается, как довольствоваться той гипотезой, что у нас есть – пусть число вычислений на интервале длиной М всегда равняется m. На самом деле, мы таким образом завысим нашу оценку. Но с чего-то надо начать.
Вообще, такие оценки далеко не всегда легко выполнять, но программист постепенно должен этому учиться. Кроме этого, как и почти все в программировании, такие оценки можно делать по-разному. Конечно – при правильно проведенной оценке конечный результат не должен изменяться. Поэтому автор сделает так, как нравится ему. Изобразим наши поисковые интервалы геометрически:
![Удвоили поисковый интервал Удвоили поисковый интервал](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/a52/04f/f16/a5204ff16b798e2b5a99e09894feca68.jpg)
Мы видим казалось бы - простую вещь: при увеличении поискового интервала в 2 раза объем вычислений увеличился в 2 раза. Однако – не совсем так. Ведь мы уже один раз проделали кусок работы на начальном интервале [1; M], а потом мы еще дважды ее выполнили. Всего получается уже
m + 2m = 3m.
Пусть нам снова не хватило простых чисел, найденных на интервале [1; 2M]. Тогда мы переходим к интервалу [1; 4M]. И мы видим, что полное количество проделанных нами вычислений от самого начала работы равно m + 2m +4m = 7m.
![Еще раз удвоили поисковый интервал Еще раз удвоили поисковый интервал](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/444/b30/d65/444b30d6516a496118f151a84445a98c.jpg)
Теперь мы уже можем разглядеть, по какому закону нарастает объем вычислений – это закон геометрической прогрессии. Поскольку знаменатель этой прогрессии равен 2, сумма k членов прогрессии равна m * (2^k – 1) / (2 -1), то есть – m * (2^k - 1). Напомним: k слагаемых в нашей сумме означает, что мы k-1 раз удваивали размер поискового интервала.
Это не просто очень сильный рост, это самый плохой рост, какой только можно вообразить, и любой ИТ-специалист сразу схватится за голову: такой алгоритм просто никуда не годится. Надо искать какой-то другой метод.
Нам снова поможет довольно простая идея: давайте, вместо того, чтобы все время начинать работу заново, попробуем просто продолжать эту работу. Легко сказать, а как сделать?
Если бы мы проверяли только деление, скажем на 2 или 3, или любое другое, но одно конкретное число, мы бы шли и шли вдоль первого интервала, потом увидели бы, что одного этого интервала нам не хватае, и пошли бы дальше, и нет проблем. Но проблема в том, что проработав число 2, мы его бросаем, и работаем с числом 3, и так далее. А когда приходит время расширить интервал – мы уже ничего не помним, и поэтому не знаем, за что хвататься.
Какое число не возьми – ту же двойку – мы не знаем, откуда надо начинать новый отсчет.
Хорошая формулировка проблемы часто содержит, частично или даже полностью, путь к ее решению, и это именно наш случай. Если мы не знаем, откуда нам начинать прыгать через 2 (3, 5, 7 - и так далее) – давайте просто будем это записывать, чтобы не забыть.
Запоминать лучше в виде словаря. Словарь – это простейший объект в языках Python и JavaScript. Возможно, и в каких-то других языках тоже, но кругозор автора не бесконечен. А в этих двух языках словарь представляет собой перечень пар <ключ> : <значение>, заключенный в фигурные скобки {}:
{
<ключ_1> : <значение_1>,
<ключ_2> : <значение_2>,
<ключ_3> : <значение_3>,
…
}
В качестве ключа могут применяться разные типы данных, но мы для простоты будем считать, что у нас ключами будут числа (чаще всего применяют строки). Значение может быть вообще любое: число, строка, список, или любой другой объект (не обязательно словарь – есть еще экземпляры классов, но мы в эту область не заглядываем).
В языках Си и С++ существует похожий тип данных, называемый структурой (struct), но это менее гибкий тип данных, чем словарь Python. Любопытно, что в JavaScript вообще все объекты, включая очень сложные, строятся на основе словарей, а классов, как в С++, Java или Python, нет вообще, но можно наследовать объекты. В этом отношении, JavaScript – самый необычный объектно-ориентированный язык.
Мы немного отклонились от нашей основной задачи, но только для того, чтобы подсветитить, насколько словарь – действительно важнейший тип данных, и далеко не в одном только Python, на котором мы решаем нашу задачу.
Что именно нам надо запоминать? Во-первых, необходимо запоминать сами простые числа – все, что мы нашли к текущему моменту. Во-вторых, для каждого такого числа надо запоминать – внимание! – последнее число, которое мы вычеркнули, удаляя числа «по поводу» этого простого числа.
Возможно, это не самая простая и понятная формулировка, поэтому давайте повторим ее другими словами. Допустим, мы нашли простое число 2. Мы последовательно вычеркивали, в нашем текущем интервале, числа: 4, 6, 8, … и остановились, например, на числе 240. А наш интервал поиска заканчивался, например, на числе 241. Дальше нам идти некуда – в рамках данного интервала.
Но ведь далее мы расширим наш интервал и пойдем дальше. Для этого мы должны знать, что в предудыщий раз мы остановились на числе 240. Остановились, потому что число 240+2=242 лежало уже вне прошлого интервала. Но оно уже лежит в новом интервале. Начиная с этого числа, можно продолжать работу. Поэтому число 240 надо запомнить, и его надо запомнить «по поводу» простого числа 2.
Запоминать эту пару чисел мы будем в виде одной записи в словаре:
{
2 : 240,
…
}
Назовем наш словарь pev_primes от слов “previous primes” – «предыдущие простые». Это название отражает тот факт, что, приступая к работе с новым интервалом, у нас уже есть запас информации, наработанный на предыдущих интервалах. На самом деле, здесь будут не только «предыдущие» числа, ведь мы будем оперативно добавлять сюда только что найденные простые числа. Но, все-таки, это название напомнит нам, что мы работаем с накопленными данными.
Вообще, всегда надо стремиться к тому, чтобы имена переменных максимально отражали содержательный смысл тех данных, которые эта переменная описывает. Это не всегда удается в полной мере, но стараться все равно надо.
Дополним наш предыдущий код заполнением словаря pev_primes по мере того, как мы проходим по начальному поисковому диапазону [1; maxNum]:
maxNum = 100
nums = list(range(maxNum + 1))
nums[1] = 0
prev_primes = {}
# внешний цикл – проход по заданному интервалу
for i in nums :
if i > 1:
# Внутренний цикл: для каждого числа i проходим «прыжками» с шагом i
for j in range(i + i, len(nums), i):
nums[j] = 0
Последнее значение j и есть то число, последнее для i, которое мы вычеркнули в текущем интервале, поэтому его надо запомнить.
prev_primes[i] = j
Давайте посмотрим, что мы сохранили:
print(prev_primes)
![Что-то пошло не так Что-то пошло не так](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/699/b35/284/699b352841b683b118b9425c6b16973e.jpg)
Это только первый вопрос, но есть еще и другой: почему значение 94 у ключа 53 такое же, как у ключа 47. Есть и третий вопрос: почему значение 94 так и повторяется до самого конца словаря pev_primes.
Обнаружить причину произошедшего на самом деле несложно. Число 53 – первое, большее половины нашего диапазона. Если мы добавим 53 к 53, мы сразу выходим за пределы поискового диапазона. Это значит, что цикл
for j in range(i + i, len(nums), i):
- даже не начнет выполняться. Тогда, что же мы будем записывать в наш словарь в инструкции prev_primes[i] = j ? Это будет то значение j, которое осталось от предыдущего i, при котором цикл for еще выполнялся – как раз 94.
Значит, нужно ввести какую-то проверку, что цикл for вообще выполняется, что поток исполнения заходит «внутрь» тела цикла. Давайте сделаем это «по-питонски»: создадим сразу список на удаление, и тогда увидим – пустой он или нет.
for i in nums :
if i > 1:
to_remove = range(i + i, len(nums)+1,
for j in to_remove :
nums[j] = 0
if len(to_remove) > 0:
prev_primes[i] = j
print(prev_primes)
![Теперь стало лучше Теперь стало лучше](https://webcf.waybackmachine.org/web/20220305225452/https://habrastorage.org/getpro/habr/upload_files/097/cf6/2fb/097cf62fbcd84ed7e3d096ee6dbf68a9.jpg)
Теперь все хорошо, нет «паразитных» записей в нашем словаре. Настал момент научиться использовать накопленные данные при расширении поискового диапазона. Давайте оформим эту работу в виде функции, которой мы будем передавать в качестве аргументов границы очередного поискового диапазона (минимальное minNum и максимальное число maxNum в этом диапазоне), а также имеющийся на данный момент словарь найденных простых чисел prev_primes :
def add_primes(minNum, maxNum, prev_primes) :
# создаем список, отображающий новый поисковый интервал
nums = list(range(minNum, maxNum+1))
# получаем сразу - в виде кортежа (prime, last_removed)
# - простое число и его «последнее удаленное»
for prime, last_removed in prev_primes.items():
Cписок на удаление создается чуть более хитро, чем ранее: мы добавляем к «последнему удаленному» его ключ - простое число. Ведь последнее удаленное было в предыдущем интервале, а следующее за ним уже не помещалось в тот интервал, но зато оно как раз попадает в новый (текущий) поисковый интервал. С него и начнем удалять:
to_remove = range(last_removed + prime, maxNum+1, prime)
И удалять числа придется тоже чуть похитрее, чем раньше: чтобы получить индекс нужного числа в списке nums, нужно из этого числа вычесть начало диапазона:
#def add_primes(minNum, maxNum, prev_primes) :
# …
# for prime, last_removed in prev_primes.items():
# …
for num in to_remove :
nums[num - minNum] = 0
Снова запоминаем «последнее удаленное» число, а это ни что иное, как последний элемент в списке на удаление:
#def add_primes(minNum, maxNum, prev_primes) :
# …
# for prime, last_removed in prev_primes.items():
# …
prev_primes[prime] = to_remove[-1]
Итак, что мы сделали. Мы продолжили вычеркивание чисел, кратным тем простым, которые мы нашли на предыдущем интервале, точнее – на всех предыдущих интервалах. Мы же будем продолжать расширять поисковый интервал неопределенно долго. Но на текущем интервале должны найтись какие-то новые простые числа в списке nums!
def add_primes(minNum, maxNum, prev_primes) :
nums = list(range(minNum, maxNum+1))
…
# ищем новые простые
for num in nums :
if num != 0:
Первое же встречное, но обязательно ненулевое – т.е. не вычеркнутое число, и есть простое, как и раньше.
next_prime = num
Как и раньше, удалять надо с числа next_prime + next_prime, с шагом next_prime.
to_remove = range(next_prime * 2, maxNum+1, next_prime)
for num1 in to_remove:
nums[num1 - minNum] = 0
Как и раньше, к данному простому числу next_prime привязываем то число, которое мы вычеркнули последним:
if len(to_remove) > 0:
prev_primes[next_prime] = to_remove[-1]
Наша главная функция готова, но нам надо написать функцию-обертку, которая будет многократно вызывать функцию add_primes(). Нужно найти n первых простых чисел. Но важно понимать, что мы не сможем сразу найти в точности n чисел, потому что мы заранее не знаем, как меняется частота простых чисел на числовой оси, и мы об этом уже говорили. У нас неверняка будет «перелет», т.е. мы найдем не менее n чисел, но это тоже вовсе не плохо.
#Находит не менее первых n простых чисел
def get_n_primes(n):
#начальный поисковый диапазон – от 2 до 2*n
minNum = 2
maxNum = 2*n
prev_primes = {}
# будем считать, сколько раз мы раздвигаем начальный интервал
cnt = 0
while len(prev_primes) < n:
cnt += 1
print(cnt)
# ключи словаря и есть те простые числа, что мы нашли
# на данный момент
print(list(prev_primes.keys()))
add_primes(minNum, maxNum, prev_primes)
# расширяем интервал: добавляем справа такой же по длине
minNum = maxNum + 1
maxNum *= 2
return list(prev_primes.keys())
Наконец, выполняем все вместе:
a = get_n_prime(100)
Выше уже говорилось, что мы не можем так подгадать, чтобы найти ровно требуемое (например, 100) количество простых чисел. Но мы можем проверить, что нашли не меньше, и в этом месте прекращаем развдигать наше раздвижное сито. И, чтобы вывести ровно 100 чисел, просто возьмем срез от найденного списка:
a = a[:100]
print(a)
Однако, эта программа почему-то «зависает»: мы увидим, что счетчик итераций cnt безостановочно увеличивается, а список найденных простых чисел не растет:
…
14
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
15
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
16
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
Мы видим нечто такое, на что вовсе не расчитывали: набор простых чисел перестал расти. Как понять – что мы делаем не так? Добавим отладочную печать в том месте, где мы пополняем наше хранилище. Будем выводить найденное простое число и то «последнее вычеркнутое», которое с ним связано.
for num in nums :
if num != 0:
…
next_prime = num
print(next_prime, end=' : ')
…
if len(to_remove) > 0:
print(to_remove[-1], end='\n')
prev_primes[next_prime] = to_remove[-1]
Получаем такую трассировку нашей программы:
…
2 : 200
3 : 198
…
89 : 178
97 : 194
101 : 103 : 107 : 109 : 113 : …
Итак, что происходит? Новые простые числа – появляются, но мы ничего не записываем «по этому поводу». Почему? Есть только одна возможность: не выполняется условие if len(to_remove) > 0, то есть – список to_remove попросту пустой, но почему? Сразу сообщим правильную догадку (к ней, в самом деле, несложно прийти): next_prime * 2 (он же next_prime + next_prime) в какой-то момент оказывается больше maxNum, и список (точнее, итератор) to_remove оказывается пустым. Список пустой – значит, ничего и не вычеркиваем, работа дальше не идет.
Точнее говоря – работа идет, но недостаточная: мы вычеркиваем числа, кратные только ранее найденным простым, а новые простые мы не находим и не и не используем.
То есть, мы с самого начала - по умолчанию - решили, что «последнее вычеркнутое» число мы будем запоминать, только если оно находится в пределах текущего поискового интервала (меньше maxNum), но так может вовсе и не оказаться. Идея была разумная: как мы можем запомнить «последнее вычеркнутое», если мы его вовсе не вычеркивали! Мы же можем вычеркнуть только в пределах текущего поискового интервала.
Значит, нам необходимо запоминать «последнее вычеркнутое» даже в том случае, если мы не вычеркивали. Ого! Вот это авантюра, вот это приписки! На самом деле, ничего страшного – просто, мы его вычеркнем потом – когда перейдем от текущего поискового интервала к следующему. Для этого, создавать список на удаление to_remove каждый раз придется чуть более аккуратно, чем ранее.
Это окажется последней настройкой нашей задумки, программа заработает уже полностью правильно, в чем легко может убедиться любой начинающий кибернетик – программист галактических искусственных разумов. Последние добавления в программу выделены отдельно.
def add_primes(minNum, maxNum, prev_primes):
'''добавляем простые числа и последние вычеркнутые числа'''
nums = list(range(minNum, maxNum+1)
for prime, last_removed in prev_primes.items():
if last_removed < minNum :
to_remove = range(last_removed + prime, maxNum+1, prime)
else :
to_remove = range(last_removed, maxNum+1, prime)
for num in to_remove :
nums[num - minNum] = 0
prev_primes[prime] = to_remove[-1]
# ищем новые простые
for num in nums :
# если равен нулю, то число вычеркнуто
if num != 0:
next_prime = num
to_remove = range(next_prime * 2, maxNum+1, next_prime)
for num1 in to_remove:
nums[num1 - minNum] = 0
if len(to_remove) > 0:
prev_primes[next_prime] = to_remove[-1]
else :
prev_primes[next_prime] = 2 * next_prime
def get_n_primes(n):
#Находит первые n или больше первых простых чисел
minNum = 2
maxNum = 2*n
prev_primes = {}
while len(prev_primes) < n:
add_primes(minNum, maxNum, prev_primes)
minNum = maxNum + 1
maxNum *= 2
return list(prev_primes.keys())
a = get_n_prime(100)
a = a[:100]
print(a)
Проверяем результат: (можете сравнить с готовыми таблицами простых чисел):.
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541]
Желающие могут получить готовый код на onlinegdb.com: https://onlinegdb.com/sQhflSeJ3
Напоследок – вопрос для размышлений, на будущее. Хорошо, мы справились с задачей. Но мы справились типично «по-питонски»: с использованием словарей. Не все языки поддерживают такой тип данных. Как упоминалось выше, в языке Си есть аналог в виде struct, но это совсем не то – это можно считать словарем, но фиксированной длины, а в Python словарь «резиновый». Как построить алгоритм «раздвижного решета» для языка Си или С++?
Литература:
1. Эратосфен. Статья в электронной энциклопедии «Википедия» https://ru.wikipedia.org/wiki/Эратосфен
2. Гончаренко В.Е. «Определение простых чисел от века папируса до века ПК», журнал «Потенциал» № 1, 2010 г.
3. Гончаренко В.Е. «Алгоритм создания коллекции простых чисел», журнал «Потенциал» № 3, 2010 г.