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

в 9:16, , рубрики: Алгоритмы, КодоБред, математика, оптимизация, оптимизация программ, числа фибоначчи

Цель

Мы хотим находить 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 << "Finishn";
}

Программа считывает число из консоли и записывает ответ в файл 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%).

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

Автор: KukarekusUltra

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js