Flat Preloader Icon

Project Euler (answers)

Проект Эйлера: Ответ и решение 10 задачи (Сложение простых чисел)

Проект Эйлера (10 задача) из тех, которые наивным перебором уже не решаются. Данный вариант решения – через “решето Эратосфена”

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

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

Сумма простых чисел меньше 10 равна 2 + 3 + 5 + 7 = 17.

Найдите сумму всех простых чисел меньше двух миллионов.

Решим любые Ваши задачи))

Вариант решения простым перебором - проект Эйлера (10 задача)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Сохраняем статус чисел в массиве

Для хранения статуса чисел создаем массив и заполняем его нулями

Нули, в данном случае будут указывать, что статус числа или не определен или число простое: массив[число] = 0.

При этом статус числа 1 в данном случае будет означать  составное число: массив[число] = 1.

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

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

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

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

  • #include <time.h> 

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

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

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

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

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

Алгоритм работы программы

  • В цикле for перебираем числа от 2 до 2000000 (LEN_ARR).
  • Если число составное (потому что его статус в массиве равен 1), то пропускаем его и переходим к следующему.
  • Число с неопределенным статусом (0) проверяем.
  • В случае, если число простое, то оно в итоге плюсуется к сумме остальных простых чисел. Кроме того, в массиве отмечаются все числа, кратные ему как составные.

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

С помощью директивы препроцессора define в начале программы:
  • #define LEN_ARR 2000000
…во всей программе строка LEN_ARR будет заменяться на цифры 2000000

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

Любое значение num, отличное от нуля, будет восприниматься как true и код после if будет выполняться

! инвертирует значение num: true в false и наоборот

Логическое “И” возвращает true только, если true && true

Тоже самое, что и answ = answ + num

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

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

Что такое составное число

Для начала напомню  понятия.

  • Простое число делится только на единицу и на само себя.
  • Составное число делится и на другие числа.

Потому в ряду натуральных чисел 2, 3, 4, 5... все числа либо простые, либо составные.

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

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

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

Например, возьмем небольшое число (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. 

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

Для хранения составных чисел создается массив, в котором индекс – это значение числа. При этом, напомню, что в массиве сохраняются только два значения: 1составное число, 0простое.

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

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

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

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

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

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

  • Функция 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), следовательно числа делятся нацело
  • !comp_arr[div] сработает, если comp_arr[div] == 0, т.е. число простое
То же самое, что div = div + 1

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

Функция, отмечающая составные числа в массиве - проект Эйлера (10 задача)
Функция отмечающая составные числа в массиве на основе решета Эратосфена

Данная функция (is_composite()) описана выше. Определяет не только составные числа (как true), но и простые (возвращает false). 

После чего функция note_composite() принимает, определенное таким образом, простое число (step) и отмечает в массиве все числа, делимые на него как составные. Просто отмечается числа следующее за start с шагом 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);

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

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

Ответ на 10 задачу проекта Эйлера в итоге составил 142913828922, программа работает довольно быстро –  примерно 270 миллисекунд. 

При прочих равных запускал этот же код без использования “решета Эратосфена” – работал примерно на 0.1 секунды дольше. Думаю, при больших объемах вычислений разница будет больше.

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

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

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

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

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

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

© 2023-2024 | eulerproject.ru