Flat Preloader Icon

Project Euler (answers)

Проект Эйлера: Ответ и решение 7 задачи (10001-е простое число)

Проект Эйлера (7 задача) не решается с наскоку. Привел два варианта решения задачи: продвинутый классический и с помощью “решета Эратосфена”

Узнать больше в телеграм-канале: ProjectEuler++

Условия задачи

Выписав первые шесть простых чисел, получим 2, 3, 5, 7, 11 и 13. Очевидно, что 6-е простое число – 13.

Какое число является 10001-м простым числом?

Всегда Вам рады)

Классический вариант решения - проект Эйлера (7 задача)

Поиск ответа обычным перебором

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

Потому, этот вариант и назван мной “классическим“.

Наивный алгоритм определения простого числа

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

В итоге, чтобы проверить, является ли число 5 простым, нужны следующие операции:

  • делим 5 на 2 и получаем остаток от деления 1;
  • 5 делим на 3 (остаток2);
  • и в конце 5 делим на 4 (остаток1).

Убедившись, что ни одно число не делит 5 нацело, можно сделать вывод, что оно простое.

Конечно, в программе эти операции будут выглядеть в виде одной строчки кода. Однако это не отменяет того, что для проверки числа 1000000 понадобится миллион операций деления. А для числа перед ним – 999999 еще почти миллион операций.

В итоге использование простого алгоритма приводит к миллиардам операций.

Продвинутая функция для определения простого числа

Продвинутая функция для определения простого числа - проект Эйлера (7 задача)
Продвинутая функция для определения простого числа (задача Эйлера 7)

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

Во-первых, нет необходимости делить на все числа от единицы до самого числа при проверке. 

Например, возьмем небольшое число (12) и будем раскладывать его на множители:

  • 2 * 6
  • 3 * 4
  • 4 * 3
  • 6 * 2

Ясно видно, что первое и второе множества повторяют третье и четвертое. Только множители меняются местами.

Потому, чтобы определить является ли число (12) простым, можно не делить его на весь ряд чисел: 2, 3, 4 10, 11. Потому что достаточно проверить до корня квадратного из числа (sqrt(12) = 3.464). В данном случае это числа 2, 3, 4.

В итоге, для 1000000 (к примеру) достаточно будет проверять делимость только до 1000 (а не до 1000000). Уже ощутимая оптимизация!

Во-вторых, функцию sqrt() – вычисления квадратного корня из числа необходимо вынести из цикла for() (смотрите фото выше). Это необходимо потому, что некоторые компиляторы могут решить вычислять ее на каждой итерации. Даже в языке Си эта функция довольно таки трудозатратна, что уж говорить про другие.

В-третьих, проверять делимость следует только простыми числами.

К примеру, если число не делится на 2, то нет смысла проверять делимость на 4. Делаем так потому, что если число делится на 4, то оно автоматически  делится и на 2. 

Реализовано это следующим образом:

  • Для хранения простых чисел создается массив, в котором индекс – это значение числа. При этом в массиве сохраняются только два значения: 0составное число, 1простое.
  • массив[число] = 0; //составное число
  • массив[число] = 1; //простое
  • Каждое вычисленное простое число заносится в массив, который в дальнейшем используется самой функцией.

* Примечания:

Подключаем стандартную библиотеку языка Си для работы с булевым типом данных.

Для стандарта  C99 булевый тип _Bool, который принимает значения 0 и 1. Данная библиотека позволяет использовать более привычный тип данных bool со значениями false и true.

Подключаем стандартную математическую библиотеку языка Си. Необходима для работы функции sqrt()

Директива макропроцессора #define заменяет в тексте программы строку LEN_ARR на цифры 200000 перед компиляцией

  • Функция sqrt(num) возвращает квадратный корень из num (внимание!) в вещественном виде. 
  • int div_max = sqrt(num) – присваиваем вычисленное значение переменной div_max (внимание!) в целочисленном виде.
  • Т.е. от числа вида 12.34567 отбрасывается дробная часть – 12
  • Это необходимо т.к. for() работает только с целочисленными значениями
  • Условие будет срабатывать и будет работать код после if(), только в случае если оба члена выражения истинны (true && true).
  • prime_arr[num] – любое ненулевое число внутри оператора if() будет восприниматься как true (истина).
  • (num % div) – любое ненулевое число внутри оператора if() будет восприниматься как true (истина).
  • ! – операция “не” инвертирует значение true в false и наоборот.
  • Таким образом !(num % div) – сработает если остаток от деления будет равен нулю (num % div == 0), следовательно числа делятся нацело
То же самое, что div = div + 1

Описание работы программы (классическое решение)

проект Эйлера (7 задача) - классическое решение
Классическое решение задачи Эйлера 7

Как уже описано выше, для хранения чисел создаем массив:

  • массив[число] = 0; //составное число
  • массив[число] = 1; //простое

Явно указываем, что число 2простое, чтобы функция is_simple() не пропустила его при заполнении массива.

  • prime_arr[2] = 1;

Далее в цикле while() перебираются числа и по порядку проверяются. Каждое простое число подсчитывается. В итоге цикл прерывается при достижении искомого счета (10001). 

* Примечание:

Ключевое слово static указывает, что локальная переменная является статической. Она расположена не в стековом фрейме (с ограниченным объемом памяти), а в основной памяти устройства. В данном случае это необходимо для размещения довольно таки большого массива.

Измеряем время работы программы

Для измерения времени работы программы подключаем библиотеку time.h:

  • #include <time.h> 

С помощью функции clock() измеряем время начала (begin) и окончания (end) работы программы. В итоге время работы будем находить как разность между ними.

* Примечания:

clock_t это тип данных, определенный через typedef. По сути это обычный целочисленный long

(double) приводит число (в данном случае – результат вычислений end – begin) к вещественному типу данных double (числа с точкой)

CLOCKS_PER_SEC – это константа, сохраненная в библиотеке и зависящая от типа устройства, на котором запускается программа. В данном случае – это просто 1000

Решение с помощью "решета Эратосфена" - проект Эйлера (7 задача)

Описание идеи алгоритма

В этом варианте решения делаем все наоборот. Находим не простые числа, а составные.

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

  • классическим способом находим первое простое число – 2;
  • отмечаем числа, делимые на 2 как составные, т.е. числа 4, 6, 8 можно уже не проверять, потому что уже заранее известно, что они не простые;
  • то же самое делаем с 3 и числами делимыми на 3 (6, 9);
  • 4 уже пропускаем, переходим сразу к 5;

В итоге, для ряда чисел до 1000000, к примеру, уже на первой итерации (с 2) 500000 откинутся, потому что они делятся на 2. Их не нужно будет проверять классическим способом.

Описание работы программы (решето Эратосфена)

Решение задачи с помощью решета Эратосфена - проект Эйлера (7 задача)
Решение задачи с помощью решета Эратосфена (задача Эйлера 7)

Определяем статус первых 1000 чисел в массиве с помощью функции is_composite()

Так же как и в классическом варианте решения, создаем массив для хранения статуса чисел. Однако статус числа 1 в данном случае означает не простое, а составное число:

  • массив[число] = 1; //составное число
  • массив[число] = 0; //простое

С помощью функции is_composite() определяется статус чисел в массиве до 1000.

При этом счетчиком cnt_comp подсчитываются составные числа. Поскольку между от числа 2 до 1000 находится 998 чисел, потому количество простых чисел в этом диапазоне определяется как разность 998 – cnt_comp.

Функция для определения составного числа (решето Эратосфена)

Функция для определения составного числа - проект Эйлера (7 задача)
Функция для определения составного числа

Работает функция точно также как и функция для определения простого числа. Однако есть отличия:

  • в массиве чисел, статус которых уже определен, 1 отмечены составные числа, а не простые;
  • потому и функция возвращает true для составного числа;
  • также функция пропускает двойку, иначе она сочтет ее составным числом.

* Примечания:

Подключаем стандартную библиотеку языка Си для работы с булевым типом данных.

Для стандарта  C99 булевый тип _Bool, который принимает значения 0 и 1. Данная библиотека позволяет использовать более привычный тип данных bool со значениями false и true.

Подключаем стандартную математическую библиотеку языка Си. Необходима для работы функции sqrt()

Директива макропроцессора #define заменяет в тексте программы строку LEN_ARR на цифры 200000 перед компиляцией

  • Функция sqrt(num) возвращает квадратный корень из num (внимание!) в вещественном виде. 
  • int div_max = sqrt(num) – присваиваем вычисленное значение переменной div_max (внимание!) в целочисленном виде.
  • Т.е. от числа вида 12.34567 отбрасывается дробная часть – 12
  • Это необходимо т.к. for() работает только с целочисленными значениями
То же самое, что div = div + 1
  • Условие будет срабатывать и будет работать код после if(), только в случае если оба члена выражения истинны (true && true && true).
  • comp_arr[num] – любое ненулевое число внутри оператора if() будет восприниматься как true (истина).
  • (num % div) – любое ненулевое число внутри оператора if() будет восприниматься как true (истина).
  • ! – операция “не” инвертирует значение true в false и наоборот.
  • Таким образом !(num % div) – сработает если остаток от деления будет равен нулю (num % div == 0), следовательно числа делятся нацело
  • !(comp_arr[div] – сработает если comp_arr[div] == 0, т.е. число div – простое

Определяем статус следующих чисел в массиве с помощью функции note_composite()

Функция note_composite() уже работает на выше описанном алгоритме “решета Эратосфена”.

  • Обнуляется счетчик составных чисел cnt_comp.
  • На основе простых чисел, уже определенных в массиве от 2 до 1000, отмечаются составные числа в массиве между индексами 1000 и 2000.
  • При этом количество составных чисел (между 1000 и 2000) подсчитывается.
  • В итоге, вычисляется количество простых чисел между 1000 и 2000 как разность 1000cnt_comp.

Далее цикл повторяется, пока не превзойдет искомую цифру количества простых чисел (10001). На основе чисел с уже определенным статусом отмечаются числа между 2000 и 3000, 3000 и 4000 и т.д. 

Функция отмечающая составные числа в массиве на основе решета Эратосфена

Функция отмечающая составные числа в массиве на основе решета Эратосфена
Функция отмечающая составные числа в массиве на основе решета Эратосфена

Функция принимает простое число (step) и отмечает в массиве, все числа, делимые на него как составные. Просто отмечается первое число (start), потом следующее (start + step) с шагом step.

Единственный интересный момент здесь в определении первого числа, с которого функция начнет отмечать.  Потому что, если простое число это 2, а отмечать нужно с 1000, то никаких проблем нет. Отмечаем числа 1000, 1002, 1004 и так далее. Однако если простое число 3, то нельзя начинать отмечать с 1000, потому что 1000 не делится нацело на 3. Потому вычисляется смещение (shift), с помощью которого получается первое число – в конкретном случае 1002. В итоге отмечаются числа 1002, 1005, 1008 и так далее.

В процессе работы функция подсчитывает количество отмеченных в массиве составных чисел и в итоге это количество возвращает.

* Примечание:

Используем тернарный оператор. Выражение можно преобразовать следующим образом:

if(start % step == 0)
shift = 0 ;
else
shift = step – (start % step);

Находим искомое значение 10001-го простого числа с помощью функции get_prime()

В процессе поиска диапазона, в котором находится искомое простое число, ряд чисел с определенным статусом увеличивается на 1000 с каждой итерацией. На каждой итерации обновляется счетчик уже определенных простых чисел cnt_prime. Однако при этом его предыдущее состояние (до итерации) сохраняется в другой счетчик past_cnt_prime.

В итоге на последней итерации мы получим (условно) следующие данные.

  • start = 100000, finish = 101000 – в этом диапазоне находится искомое 10001-е простое число;
  • cnt_prime = 10023 – столько простых чисел найдено в диапазоне от 2 до 101000, что и стало сигналом окончания цикла while();
  • past_cnt_prime = 9988 
  • столько простых чисел найдено в диапазоне от 2 до 100000;
  • в итоге, искомое 10001-е простое число – это 13-е (10001 – 9988) простое число, найденное после 100000.

Функция возвращающая значение простого числа по его порядковому номеру

функция считает количество простых чисел и возвращает значение простого числа из отрезка массива по его номеру
Функция возвращающая значение простого числа по его порядковому номеру

Тут все просто. На заданном отрезке массива функция считает простые числа и в итоге возвращает значение с искомым номером.

Проект Эйлера (7 задача) - ответ и скорость работы программы

проект Эйлера (7 задача) - ответ
7 задача ответ

В итоге ответ на 7 задачу проекта Эйлера составил 104743, программа работает довольно быстро 7 – 8 миллисекунд.  Классический вариант программы считает примерно так же.

Пока прибавка скорости значительная только относительно наивного алгоритма поиска простых чисел – простым перебором. В последующих, более сложных задачах проекта Эйлера разница между продвинутым классическим вариантом и алгоритмом через решето Эратосфена будет более заметна.

По традиции хотелось бы выразить глубочайшую признательность Сергею Балакиреву за его титанический труд по созданию бесплатных курсов и в частности, за “Язык программирования C/C++ для начинающих“.

проект Эйлера
Курс "C/C++ для начинающих"

Есть вопросы, господа? Отвечаем спокойно, четко и только по делу))

Есть вопросы, господа?

Проект Эйлера (ответы) | © 2023-2024 | eulerproject.ru

Проект Эйлера (ответы) | © 2023-2024 | eulerproject.ru

© 2023-2024 | eulerproject.ru