Flat Preloader Icon

Project Euler (answers)

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

Проект Эйлера (29 задача) столкнет Вас с типичными проблемами больших чисел. Однако, вполне возможно получить ответ менее чем за 0.1 секунды.

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

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

Рассмотрим все целочисленные комбинации ab для 2 ≤ a ≤ 5 и 2 ≤ b ≤ 5:

22=4, 23=8, 24=16, 25=32
32=9, 33=27, 34=81, 35=243
42=16, 43=64, 44=256, 45=1024
52=25, 53=125, 54=625, 55=3125

Если их расположить в порядке возрастания, исключив повторения, мы получим следующую последовательность из 15 различных членов:

4, 8, 9, 16, 25, 27, 32, 64, 81, 125, 243, 256, 625, 1024, 3125

Сколько различных членов имеет последовательность ab для 2 ≤ a ≤ 100 и 2 ≤ b ≤ 100?

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

Решение (проект Эйлера - 29 задача)

Проект Эйлера (29 задача) - решение, часть 1
Решение, часть 1

Принцип хранения больших чисел

Поскольку функция main() получилась довольно-таки объемной, потому решил не “мельчить” и разбить ее на два скриншота.

Чтобы была возможность оперативно менять значения без поиска по всему коду, потому пределы вычислений BASE (последнее число) и POW (самая большая степень) записал через #define. Кроме того, это позволяет использовать эти значения в функциях без передачи их в качестве аргументов.

Для хранения чисел использовал двумерный массив arr[BASE * POW][BASE + 1] (смотрите выше на фото). Потому что 100 (BASE ) чисел в 100 (POW) степенях могут дать 10000 чисел, и нам понадобится столько же массивов длиной также 101. Чтобы такой большой массив не переполнял стек, воспользуемся static для размещения его в основной памяти компьютера.

Хранение чисел в виде массива цифр

Чтобы понять суть проблемы работы с большими числами, приведенными в задании, приведу пример. 

Так 21 в степени 7 это 1801088541 и его можно хранить как int. Однако 100 в степени 100 – это уже 200-значное число. Как такие числа хранить, в виде массива цифр?

Если 21^7 (1801088541 ) в массиве будет выглядеть как ряд цифр [1,8,0,1,0,8,8,5,4,1], то как будет выглядеть умножение? При возведении в следующую степень (21^8) каждую цифру потребуется “столбиком” перемножать на 21. При этом понадобится переносить остатки. Так 21 * 4 = 84, 4 остается в этом регистре, остаток (8) переносится в старший.

Как сравнивать два числа? Придется поэлементно проходить по двум массивам и сравнивать каждую цифру.

В общем, в итоге, требуется не только много места для хранения чисел, но и огромное количество операций для операций с ними.

Хранение чисел в виде массива множителей

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

Так число 3^7 будет храниться не как 2187 и не как массив цифр [2,1,8,7], а как массив множителей [0,0,0,7,0,0,0..].

arr[3] = 7 – означает, что число имеет 7 множителей 3 (3*3*3*3*3*3*3).

В итоге, чтобы возвести 3^7 в следующую степень 3^8, необходимо просто увеличить соответствующий индекс в массиве arr[3]++.

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

Первое ускорение алгоритма (проект Эйлера - 29 задача)

При возведении разных чисел в разные степени может получиться одно и то же число. Так, например, 2^4 = 16 и 4^2 = 16. 

Однако, при сравнении массивов они будут разными:

  • [0,0,4,0,0,0…] //2^4 –  четыре двойки – 2*2*2*2;
  • [0,0,0,0,2,0…] //4^2 – две четверки – 4*4;

Потому, во-первых, перед сохранением числа в массив, его необходимо разложить простые множители. Так 4 = 2^2 и, следовательно, 4^2 = (2^2)^2= 2^4.

Во-вторых, простые числа уже нельзя разложить как либо еще по определению. Потому, числа, образованные степенями простых чисел, можно не проверять на наличие пары, а сразу же учитывать как уникальные.

До 100 существует 25 простых чисел. Занесем их в массив по индексу при его инициализации (смотри скриншот выше).

  • prime_arr[3] = 1; // число 3 – простое
  • prime_arr[4] = 0;  //число составное
Таким образом, в итоге, экономится 25% времени на проверку уникальности числа.

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

Проект Эйлера (29 задача) - решение, часть 2
Решение, часть 2

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

Далее, как описано было выше, число не заносится сразу в массив. Сначала число раскладывается на простые множители.

Так вместо 21^4 в память заносится 3^4, после чего 7^4. Т.е. в итоге число 21^4 записывается не как 21*21*21*21, а как 3*3*3*3*7*7*7*7 ([0,0,0,4,0,0,0,4,0,0…] в массиве).

Второе ускорение алгоритма (проект Эйлера - 29 задача)

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

Например, вышеупомянутое число 21^4 запишем в итоге так:

  • [8,12,0,4,0,0,0,4,0…], где
  • arr[0] = 8 множителей (4 тройки плюс 4 семерки);
  • arr[1] = 12 (3 + 7) – сумма номиналов множителей.

Далее число проверяется на его уникальность.

Если оно образовано степенью простого числа – оно уникально, дополнительных проверок не потребуется.

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

Если число не уникально, то оно обнуляется и перезаписывается. Иначе оно сохраняется и учитывается.

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

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

  • #include <time.h> 

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

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

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

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

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

Функции, используемые в программе (проект Эйлера - 29 задача)

Функция, которая возвращает (проект Эйлера - 29 задача)множитель числа
Функция, которая возвращает множитель числа

Функция, которая возвращает множитель числа

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

В итоге функция вернет либо минимальный делитель числа, либо само число (если оно не делится).

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

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

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

Например, при возведении числа 21^4:

  • arr[3] += 4; //в массив записывается 4 множителя тройки
  • arr[0]+= 4; //общее количество множителей стало 4
  • arr[7] += 4; //в массив записывается 4 множителя семерки
  • arr[0]+= 4; //общее количество множителей стало 8

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

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

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

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

Пример вывода сообщения об ошибке
Пример вывода сообщения об ошибке

Функция для подсчета суммы номиналов множителей

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

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

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

Функция для сравнения массивов

Функция для сравнения массивов
Функция для сравнения массивов

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

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

Функция для обнуления массива

Функция для обнуления массива
Функция для обнуления массива

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

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

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

В итоге ответ на 29 задачу проекта Эйлера составил 9183, программа считает ответ довольно быстро – за 0.1 секунды. 

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

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

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

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

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

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

© 2023-2024 | eulerproject.ru