Вычисляем миллиардное число Фибоначчи менее чем за 7 секунд

от автора

Цель

Мы хотим находить F_n где:

F_0 = 0\\ F_1 = 1\\ F_n = F_{n-1} + F_{n-2}

И хочется это делать очень быстро, абсолютно точно и со всеми знаками.

Простой алгоритм

Заметим, чтобы найти число F_{n+1} надо знать числа F_n и F_{n-1}. Значит для поиска очередного числа Фибоначчи нам нужно знать два предыдущих. Поэтому мы будем хранить пары (F_n, F_{n-1}). По этой паре можно вычислить следующую (F_{n+1}, F_{n}), а по этой следующую и тд. Таким образом по паре (F_1, F_0) = (1, 0) мы можем найти пару (F_n, F_{n-1}). И так мы нашли F_n.

С помощью такого алгоритма мы можем вычислять F_n за O(n)операций над числами фибоначчи. Но нам этого не достаточно! Ускорим алгоритм!

Ускоренный алгоритм

Теперь мы хотим по (F_n, F_{n-1}) вычислять сразу (F_{2*n}, F_{2*n-1}). Для этого воспользуемся формулой, взятой с википедии:

F_{n+m} = F_{n-1} * F_{m} + F_{n} * F_{m + 1}

Немного её преобразуем для наших нужд:

F_{2n} = F_{n + n} = F_{n-1} * F{n} + F_{n} * F_{n+1}  \\ F_{2n-1} = F_{n + (n-1)}=F_{n-1}* F_{n-1} + F_n * F_n

Заметим что  F_{n+1} = F_n + F_{n-1}и преобразуем первую формулу:

F_{2n} = F_{n-1} * F{n} + F_{n} * F_{n+1} = F_n *(F_{n-1}+ F_{n+1}) = F_n * (2F_{n-1}+ F_n)

Итоговый вид:

F_{2n} = F_n * (2F_{n-1} + F_n) \\ F_{2n-1} = F_{n-1}^2 + F_n^2

И теперь мы можем по (F_n, F_{n-1}) вычислять(F_{2*n}, F_{2*n-1}).

Но как это ускорит алгоритм? А очень просто! Нам нужно вычислить (F_n, F_{n-1}) рассмотрим два случая:

  1. Если n = 2k:

    Воспользуемся новой формулой и по (F_k, F_{k-1}) вычислим результат.

  2. Если n = 2k + 1

    Воспользуемся старой формулой и по (F_{2k}, F_{2k-1}) вычислим результат, а (F_{2k}, F_{2k-1}) мы найдем через пункт 1.

Таким образом, на каждом шаге, число n уменьшается не менее в 2 раза. Значит операций над числами Фибоначчи будет O(\log{n}). Но и в этом алгоритме есть что улучшить.

Ускоряем ускоренный алгоритм

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

Умножение длинных чисел выполняется медленно.

Для тех кому интересно, как быстро умножать длинные числа, есть крутая статья.

Значит, чтобы код работал ещё быстрее, нам нужно свести количество умножений к минимуму. Сейчас у нас используется 3 умножения. Сведём их к двум!

Немного пошаманим над F_{2*n - 1}:

F_{2*n - 1} = F_{n-1}^2 + F_{n} ^ 2 = \\ = F_{n-1}^2 + (F_{n} ^ 2 + F_n*2F_{n-1}) -  2F_n*F_{n-1} = \\ = F_n * (2F_{n-1} + F_n) - (2F_n *F_{n-1} - F_{n-1}^2) = \\ = F_n * (2F_{n-1} + F_n) - F_{n-1} * (2F_n - F_{n-1})

Вспомним что F_n * (2F_{n-1} + F_n) = F_{2n} и тогда получим:

F_{2*n - 1} =  F_n * (2F_{n-1} + F_n) - F_{n-1} * (2F_n - F_{n-1}) = \\=F_{2n} - F_{n-1} * (2F_n - F_{n-1})

Итоговый вид:

F_{2n} = F_n * (2F_{n-1} + F_n) \\ F_{2*n - 1} =F_{2n} - F_{n-1} * (2F_n - F_{n-1})

И теперь наш код использует в 3 / 2 раза меньше умножений, чем прошлый алгоритм!

И ещё небольшая оптимизация

А также в конце нам не обязательно вычислять пару чисел, поэтому будем вычислять одно. Это значит:

  1. Если n четно, мы избавимся ещё от одного умножения (при этом над очень большим числом).

  2. Если n нечетно, мы будем пользоваться формулой  F_{2n-1} = F_{n-1}^2 + F_n^2. Во первых здесь меньше операций, и во вторых здесь квадраты чисел, что скорее всего положительно влияет на производительность.

Это мне сэкономило 30 секунд при вычислении 10.000.000.000 числа.

Реализация

Я буду писать на C++ и буду пользоваться библиотекой gmp, она обычно встроена в gcc.

Хедеры которые я использовал:

#include <iostream> #include <gmpxx.h> #include <fstream>

Функция для расчета следующего числа Фибоначчи:

void fib_next(mpz_class& f2, mpz_class& f1) {   std::swap(f2, f1);   f2 += f1; }

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

void fib_double(mpz_class& f3, mpz_class& f2) {   mpz_class f6 = (f3 + f2 * 2) * f3;   mpz_class f4 = (f3 * 2 - f2) * f2;   f3 = std::move(f6);   f2 = f3 - f4; }

Функция для получения числа Фибоначчи:

mpz_class fib_get(size_t N) {   // запоминаем остаток от деления на два   bool R = N % 2;   // уменьшаем N в два раза, с учётом формул   N = (N + 1) / 2;    mpz_class a = 1;   mpz_class b = 0;      // Это номер бита в числе n (начинаем с последнего)   int i = sizeof(size_t) * CHAR_BIT - 1;      // ищем первый не нулевой бит   for (; i >= 0; --i) {     if (1ULL & (N >> i)) {       break;     }   }   // И дальше считаем   -- i;   size_t h = 1; // переменная показывает какое число фибоначи уже посчиталось   for (; i >= 0; --i) {     fib_double(a, b);     h *= 2ULL;     if (N & ((size_t)1ULL << i)) {       fib_next(a, b);       ++ h;     }          // выводим информацию, чтобы небыло скучно ждать     std::cout << "find: " << h << '\n';    }      // считаем окончательный ответ без пары   if (R) {     a = a * a + b * b;     std::cout << "find: " << h * 2 - 1 << '\n';    } else {     a = (a + b * 2) * a;     std::cout << "find: " << h * 2 << '\n';    }   return a; }

Реализация немного кривая, но я не хотел писать через рекурсию.

И main:

int main() {   size_t n;   std::cout << "Enter the fibonacci number: ";   std::cin >> n;   auto answer = fib_get(n);   std::string str = answer.get_str(16);   // записываю в 16ричной системе исчисления   // так как хочу, чтобы ответ быстрее записывало :D   std::ofstream fout("answer.txt");   fout << str;   std::cout << "Finish\n"; }

Программа считывает число из консоли и записывает ответ в файл answer.txt. Компилируется при помощи команды g++ main.cpp -lgmpxx -lgmp

Тесты

100 миллионное число Фибоначчи считает за 0.589 секунды, и файл с ответом весит 17мб.

Миллиардное число Фибоначчи считает за 6.958 секунды, и файл с ответом весит 166мб.

10 миллиардное число Фибоначчи считает за 1 минуту 12.879 секунды, и файл с ответом весит 1.7гб.

Дальше мне стало страшно тестить…

Итог

Нигде в интернете я не смог найти подсчет 10,000,000,000 числа Фибоначчи. Это была моя первая статья, надеюсь это было интересно (практическое применение 0%).

Всем удачи, всем до новых статей 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/835950/