Flat Preloader Icon

Project Euler (answers)

Проект Эйлера: Ответ и решение 8 задачи (Наибольшее произведение в последовательности)

Проект Эйлера (8 задача) легко решается “в лоб” – многократным перемножением серии чисел. Однако для нее есть более “красивое” решение.

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

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

Наибольшее произведение четырех последовательных цифр в нижеприведенном 1000-значном числе равно 9 × 9 × 8 × 9 = 5832.

Задание - проект Эйлера (8 задача)
Задание (задача Эйлера 8)

“Найдите наибольшее произведение тринадцати последовательных цифр в данном числе.”

Шелби решает задачи Эйлера
Как бы так тактично намекнуть добавить сайт в закладки?)

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

проект Эйлера (8 задача) - наивное решение
Наивное решение задачи Эйлера 8

Преобразования 1000-значного числа в массив

Первая проблема, с которой сталкиваешься в этой задаче – это длина числа. 1000-значное число в языке Си не влезет ни в один тип данных. Потому копируем число в программу в виде строки и далее преобразуем в массив чисел.

Для преобразования символов строки в числа воспользовался функцией atoi(). Однако это не самый удачный вариант, потому что эта функция служит для преобразования именно строки в число. Потому к каждому символу перед преобразованием пришлось добавлять знак окончания строки ‘\0’ и представлять именно в формате строки.

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

Перемножаем серию чисел "в лоб"

По сути, это первое, что приходит в голову, глядя на условия задачи. Перемножаешь числа с 1 по 13. Потом со 2 до 14, с 3 до 15, с 4 до 16 и так далее.  В итоге выбираешь максимальный результат.

Однако есть несколько моментов, которые можно улучшить:

  • При вычислении каждой серии из 13 чисел, необходимо произвести умножение 12 раз. При этом одни и те же пары чисел перемножаются снова и снова. Так при перемножении серий чисел с номерами: 1*2*3*4*5*6*7*8*9*10*11*12*13 и 2*3*4*5*6*7*8*9*10*11*12*13*14 множители 2*3*4*5*6*7*8*9*10*11*12*13 повторяются и вычисляются повторно.
  • 1000-значное число из задания содержит много нулей. Потому произведение 13 серий чисел попадающих на него будут давать ноль. Ноль никак не может быть ответом на задачу и эти серии вычисляются напрасно. В итоге их лучше пропускать.

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

Функция atoi(symb) служит для преобразования строки в число. Т.е. строку “0123” она преобразует в число 123. При этом:

  • char symb[ ] = {num_str[ i ], ‘\0’};
  • в качестве аргумента функция принимает именно строку – массив символов, оканчивающийся символом ‘\0’;
  • требуется подключить библиотеку #include <stdlib.h>
То же самое, что i = i + 1

То же самое, что и prod = prod * num_array[i + 1]

“%lld” спецификатор %d с суффиксами ll указывает, что тип выводимой переменной answlong long int

Более "красивое" решение - проект Эйлера (8 задача)

Преобразование строки в массив чисел
Преобразование строки в массив чисел

Преобразования 1000-значного числа в массив чисел

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

Что такое строка? По сути своей – это обычный массив чисел. Однако есть отличия:

  • В конце этого массива обязательно стоит символ окончания строки ‘\0’.
  • Цифры в таком массиве означают не сами себя, а численный код того или иного символа. Например 65 означает символ ‘А’.

Значения всех кодов хранятся в так называемой сводной таблице кодов ASCII. Однако нас интересует, что символ ‘0’ (ноль) имеет код 48, а ‘1’ (один) – 49 и т.д.

Таким образом вполне достаточно из кода символов цифр вычитать 48, чтобы получить сами цифры. В итоге функция atoi() становится не нужна.

Суть алгоритма

После того как перемножены числа с 1-го по 13-е, мы получаем произведение серии чисел от 1-го до 13-го в виде числа.

  • 1*2*3*4*5*6*7*8*9*10*11*12*13 – обозначим его условно answ_1_13.

Однако далее мы не будем заново перемножать числа от 2-го до 14-го, чтобы получить следующее произведение (answ_2_14)

Вместо этого:

  • answ_1_13 делим на 1-е число;
  • 2*3*4*5*6*7*8*9*10*11*12*13 – получаем произведение чисел от 2-го до 13-го;
  • после чего answ_1_13 умножаем на 14-е число;
  • 2*3*4*5*6*7*8*9*10*11*12*13*14 – искомое следующее произведение (answ_2_14);

В итоге 12 операций перемножения 13 чисел заменены на две: одно деление и одно умножение. При этом серия из 13-ти чисел как бы смещается на одно число вправо, “ползет как змея“.

Для ясности и краткости изложения примем некоторые условные обозначения:

  • серия перемножаемых чисел – это “змея” (думаю, похоже);
  • от серии чисел отрезается 1-е число (делится) и добавляется 14-е (умножается) – “змея ползет“;
  • процесс перемножения чисел от 1-го до 13-го – “змея растет“;
  • ну а в случае, если впереди ноль – “змея” через него “перепрыгнет“.

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

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

  • #include <time.h> 

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

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

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

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

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

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

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

Структура Snake – это непрерывный участок памяти, расположенный по определенному адресу.
&Snake – возвращает адрес в памяти данной  структуры

Тернарный оператор ? : иначе можно записать следующим образом:

if(answ > Snake.product):
answ = answ;
else
answ = Snake.product;
Т.е. в answ сохраняется максимальное из значений.

Описание структуры "змея"

Структура "змея" - проект Эйлера (8 задача)
Структура "змея" (задача Эйлера 8)

Поскольку хочется сделать “красивый” и универсальный вариант программы, воспользовался директивой препроцессора #define. С ее помощью в начале программы, моно задать как длину массива (1000), так и длину “змеи” – произведения серии чисел (13).

Описание структуры "оконечностей змеи"

Так как у “змеи” есть начало и конец:

  • […(1)*2*3*4*5*6*7*8*9*10*11*12*(13)…]

то создаем для нее структуру.

struct tip – ‘это новый тип данных и он будет описывать по сути число в массиве. Потому будет иметь два поля:

  • value – значение числа;
  • indx – и его положение (индекс) в массиве.

При этом (для красоты кода) с помощью директивы typedef изменяем название своего типа данных:

struct tip меняем на TIP.

В итоге TIP head создает структуру head типа struct tip.

К ее полям можно обратиться “через точку”:

  • head.indx = 3; // устанавливаем положение “головы” как 3 в массиве. 
  • head.value = arr[3]; // присваиваем значение “головы” т.е. копируем значение числа из массива;

Описание и инициализация структуры самой "змеи"

Также создаем новый тип данных для структуры “змея” (SNAKE). Он будет содержать:

  • две вложенные структуры head и tail (типа TIP описанного выше), описывающее состояние “головы” и “хвоста” змеи;
  • текущее произведение чисел между собой от “хвоста” до “головы” змеи включительно;
  • длину змеи” – количество чисел в серии.
SNAKE Snake;  // создаем структуру “змея”.
  • В первоначальный момент времени “змея” еще маленькая и занимает лишь одно число – первое в массиве.
Snake.product = num_arr[0]; // произведение равно первому числу массива (в нашем случае – это число 7).
Snake.len_snake = 1; // “длина змеи” пока что – одно число.
  • К полям структуры “змеиSnake мы обращаемся через точку.

Поскольку “змея” состоит лишь из одного числа, то и “голова” и “хвост” инициализируются им же. 

Snake.head.indx = 0; // положение “головы” в массиве – 0.
Snake.head.value = num_arr[0]; // первое число массива – 7.
Snake.tail.indx = 0; // “хвост” также находится в первом числе массива
Snake.tail.value = num_arr[0];
  • Здесь видно, что к полям вложенных массивов обращаемся также через точки. 

Snake.head – обращаемся к структуре head вложенной в структуру Shake.

Snake.head.indx – обращаемся к полю indx вложенной структуры head.

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

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

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

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

Описание функции "змея растет"

Функция "змея растет" - к решению задачи Эйлера 8
Функция "змея растет" - к решению задачи Эйлера 8

После инициализации структуры, “змея” занимает только лишь одно число:

  • [(7) 31671765313306…]

… и дальше ей нужно “расти”, чтобы включить в себя 13 множителей:

  • [(7316717653133)06…]

В программе это реализовано с помощью функции snake_growth(), срабатывающей, если длина “змеи” меньше 13:

  •  if(Snake.len_snake < LEN_SERIES)            
  • is_answ = snake_growth(&Snake, num_arr);

Пока “змея” не увеличится до 13 чисел:

  • while(snake->len_snake < LEN_SERIES)

Перемещаем “голову” на шаг вперед – на следующее число:

  • snake->head.indx++;  

Обновляем значение “головы” новым числом из массива:

  • snake->head.value = num_arr[snake->head.indx];

Изменяем “тело змеи” – умножаем произведение серии на следующее число:

  • snake->product *= snake->head.value;    

Увеличиваем длину змеи:

  • snake->len_snake++;

Если “змея” успешно выросла (до 13 чисел), функция возвращает true. Иначе (если “змея” встретила ноль), работа функции прерывается и возвращается false:

  • if(!snake->head.value)
  • return false; 

Так как функция принимает не структуру, а указатель на структуру:

  • snake_crawl(SNAKE *snake, char num_arr[])

То к полям структуры внутри функции будем обращаться сначала “через стрелочку” и только затем “через точку

  • snake->head.indx++;

Описание функции "змея ползет"

Функция "змея ползет" - к решению задачи Эйлера 8
Функция "змея ползет" - к решению задачи Эйлера 8

Как только длина “змеи” достигает 13 чисел:

  • [(7 316717653133)06…]

Ряд чисел начинает сдвигаться вправо – “змея ползет”:

  • [7(3167176531330)6…]

Для чего служит функция snake_crawl(), срабатывающей, если длина “змеи” достигла 13.

  • if(Snake.len_snake < LEN_SERIES)
  • is_answ = snake_growth(&Snake, num_arr);
  • else                                  
  • is_answ = snake_crawl(&Snake, num_arr);
 Работает функция следующим образом:
  • “голова змеи” движется вперед – соответствующим полям head присваиваются значения следующего в массиве числа;
  • змея подрезает хвост” – произведение серии чисел (поле product) сокращается на число из “хвоста“;
  • перемещаем “хвост” – присваиваем полям структуры tail значения следующего за ним в массиве числа.
  • если “голова” встретила ноль, то работа прерывается и функция возвращает false, иначе – true.

Описание функции "змея прыгает через ноль"

Функция "змея прыгает через ноль" - проект Эйлера (8 задача)
Функция "змея прыгает через ноль"

Если “змея” успешно “выросла” или “проползла” на одно число, то в поле product сохраняется очередной результат – произведение серии из 13 последовательных цифр. При этом соответствующие функции возвращают true, которое сохраняется во флаг is_answ.

Конструкция:

  • if(is_answ)
  • answ = (answ > Snake.product) ? answ : Snake.product;
… обновляет максимальный результат.

Если во время “роста” или “ползания” встретится ноль, то функции вернут false и в действие вступит функция snake_jamp(). “Змея перепрыгнет” через ноль и сначала будет заново “расти“, а после – “ползти“. При этом в 1000-значном числе из задания есть не только одиночные нули, но и группы последовательно стоящих нулей. Потому длина “прыжка” будет изменяться в зависимости от их количества.

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

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

Вот так, ползком и вприпрыжку “змея” проходит весь массив чисел и находит в итоге ответ на 8 задачу проекта Эйлера – 23514624000. При этом программа работает молниеносно – возвращает ответ за 0.0 секунд.

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

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

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

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

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

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

© 2023-2024 | eulerproject.ru