Comments 23
Тут нужно сделать важную оговорку, что все указанные оценки справедливы только если вы, например, делаете вычисления по модулю. Проблема в том, что длина Fn растет линейно от n из-за чего итеративный алгоритм работает на самом деле за O(n^2), а быстрый за O(nlogn) (из мастер-теоремы). Ну и соответственно если вы работаете по модулю часла длины k, то получаются оценки O(nk)/O(klogn).
Есть еще формула Бине
которая сводит вычисления чисел Фибоначчи к возведению в степень, которое можно делать за log, но из-за работы с float она не такая полезная даже для спортивного интереса, в отличии от
А еще можно вычислять по формуле Бине, но в алгебраическом расширении целых чисел корнем из 5. Тогда числа хранятся в виде a+b*sqrt(5). И умножение таких чисел будет тем же самым возведением матрицы в степень.
N log N - быстрое преобразование Фибоначчи)), БПФ
А если есть процессор с бесконечно большим числом ядер)) то за O(log(n)) парралельных шагов можно вычислять не только числа Фибоначчи, но и любую рекуррентную последовательность вида
Сведение Фибоначчи к умножению имхо проще всего понять, записав его как матричное умножение,
Вместо f(n) будем смотреть что происходит с вектором, точнее столбцом f(n+1), f(n)
:
f(n+1) = 1*f(n) + 1*f(n-1)
f(n) = 1*f(n) + 0*f(n-1)
Получаем буквально возведение в степень (мартиц)
100-километровым плотным слоем кроличьих тушек.
Напомнило нефтяную планету из кротов.
public static void main (final String... args )
А зачем final аргументам?
Как сложно вы расписали! Это классическая задача, она разбирается в легендарной книге SICP и во многих учебниках по линейной алгебре. И в матричном виде разбор её решения занимает меньше страницы текста.
К сожалению, с математикой у меня не очень. Эта статья появилась, как раз по той причине, что во время разбора упражнения 1,19 в SICP, я не смог понять, как выводится формула a ← bq + aq + ap, b ← bp + aq, а про матричный способ вычисления чисел Фибоначчи не знал. Попробовал разобраться самостоятельно...
Ясно. Ну смотрите. Линейная алгебра очень большая наука. И даже то, что дается в университетских курсах это ее малая часть. Но есть и хорошая новость. Почти все знания из линейки, которые как то можно применить на практике изучаются за недельку (или две). Это операции сложения и умножения матриц и векторов, а ещё вычисление детерминанта ну и матрицы поворота и перехода к другому базису. Это покрывает почти все практические нужды, которые только бывают. Так что если вдруг захотите разобраться, вам надо внимательно изучить первые 20-30 страниц любого учебника. Остальные 400 страниц почти нигде не пригодятся. Такой вот принцип Парето на максималках.
Хорошая традиция - каждый раз, прежде чем начинать читать статью про быстрое нахождение чисел Фибоначчи, брать и воспроизводить по памяти решение:
julia> fib(n) = ( BigInt.( [1 1;1 0] )^n )[1];
julia> fib.(1:10)
10-element Vector{BigInt}:
1
2
3
5
8
13
21
34
55
89
julia> @time fib(1_000_000);
0.029857 seconds (1.45 k allocations: 10.012 MiB)
julia> @time fib(100_000_000);
6.999292 seconds (105.82 k allocations: 1.612 GiB, 0.14% gc time)
julia> 100_000_000 |> fib |> string |> length
20 898 764
А возведение в степень здесь происходит за логарифм или линейное? Просто в этом и заключается вся задача
Кажется по выводу, что в 100 раз увеличили число, в ~100 раз увеличилось и время. Хотя возможно это вина длинной арифметики
Хотя возможно это вина длинной арифметики
Именно. Длина чисел растет линейно от n. Более того, оно так быстро работает только потому, что там еще и умножение длинных чисел сделано быстро, через FFT какое-нибудь. A то мы бы только умножение двух чисел длиной с миллионы знаков ждали бы несколько минут. В итоге там что-то около O(n (log n)^2) получается.
TL;DR
Слишком большая статья для описания такой простой вещи. Лучше б рассказали старинную байку, как фанаты ассемблера и си зарубились из-за скорости, договорились помериться, вычисляя n-е число Фибоначчи, и всех уделал какой-то чувак, написавший чуть ли не на бейсике.
Как только узнаёшь в задаче возведение матрицы в степень – остальное должен написать, не задумываясь.
Кстати, если копнуть глубже – это решение не за логарифмическое время. Ведь числа с ростом N становятся всё длиннее, и умножение, соответственно, медленнее. Исходя из объёма вашей статьи и упоминания в ней быстрого алгоритма умножения – я ожидал увидеть как минимум алгоритм Карацубы.
Почему бы просто не воспользоваться формулой Бине?
Как так получилось, что итеративный способ с n сложениями оказался дольше/хуже, чем матричное возведение в степень, где как я понимаю 4 операции сложения и 8 операций умножения на каждую итерацию⁉️ ?
Я "думал-думал и всё понял" ?
В матричном умножении для быстрого вычисления используется быстрое возведение в степень: https://habr.com/ru/companies/otus/articles/779396/
Что логарифмически уменьшает количество операций.
Без каких-либо претензий, предлагаю такой вариант.
// быстрое вычисление чисел Фибоначчи для х => 0 по формуле Бине
public static Int64 FastFibonacci(int x)
{
const float cp = 1.6180339887498948482046f; // (1+sqrt(5))/2
const float sqrt5 = 0.4472135954999579392818f; // 1/sqrt(5)
const float delta = 0.4999999999f; // для округления вправо
float s = sqrt5;
float v = cp;
int c = x;
while (c != 0)
{
if (c % 2 == 1)
{
s = s * v;
}
c = c >> 1;
v = v * v;
}
return (Int64)(s + delta);
}
Прошу не судить строго, может чего не понял.
Тут проблема с точностью. float всего-то 32 бита. Но даже в даблах у вас быстро точностьи мантиссы перестанет хватать. раньше, чем int64 ответ переполнится. А уж если в ответ длинные числа нужны, то вы длинную вещественную арифметику писать замучаетесь.
Согласен по всем статьям. Во-первых, действительно, если и использовать стандартные типы, то лучше будет double - там 16 значащих цифр. Во-вторых, округление результата при помощи delta и приведения типа к Int64 имеет смысл до 1Е17 максимум (для использования типа double), а дальше можно вообще не париться с округлением, и просто s возвращать.
Для повышения точности можно использовать тип BigInteger, тогда вроде всё будет работать бог знает до куда, главная проблема будет вычислить соответствующее количество значащих цифр для констант (1+sqrt(5))/2 и 1/sqrt(5).
Показанный выше алгоритм я изначально планировал как быстрый, но с ограниченной точностью. Главный вопрос, какова от него от такого польза? Вечный вопрос, нужно ли вообще писать алгоритм, если заранее не знаешь, понадобится ли он вообще кому-нибудь?
Быстрое нахождение чисел Фибоначчи