Как стать автором
Обновить

Комментарии 46

А разве нет просто формулы для n-го числа Фибоначчи, чтоб вычислять за O(1)?

Можно через золотое сечение вычислить, но там возможны погрешности.

К сожалению нет(

F(n) = \frac{1}{\sqrt{5}} \left( \left( \frac{1 + \sqrt{5}}{2} \right)^n - \left( \frac{1 - \sqrt{5}}{2} \right)^n \right)

Есть такая формула!

Тут используется возведение в степень, оно работает не за константное время

Вы правы! Формулы для вычисления за O(1) действительно нет. Но красота формулы и ее вывода стоят упоминания здесь.

Умножение длинных чисел, и даже сложение или копирование тоже работает не за константное время, потому что числа имеют длину O(n) :-)

В статье у вас этот момент обойдён, потому что вы пишете O(log n) операций над числами Фибоначчи. Если возведение в степень тоже считать операцией – получим O(1). А если честно считать сложность по времени – то у вас что-то вроде O(n*log n*...) получается (что под многоточием – определяется выбранным алгоритмом умножения), и вроде это предел (если какой-нибудь гений не изобретёт алгоритм вычисления чисел Фибоначчи цифра за цифрой или типа того).

Забавно, что мой коммент ниже, где я упомянул реальную временную сложность с учётом длины чисел, нахватал минусов, кто-то даже не поленился в карму минуснуть.

Причём второе слагаемое меньше 1 и его можно не вычислять, отбрасывая дробную часть от первого слагаемого.

Надо прикинуть, с какой точностью sqrt(5) вычислять придётся.

Если приближённо вычислять через быстрое возведение в степень 2**(n*log2(x)) - да, кажется вообще быстро. Но чтобы считать точно, придётся экспоненту ручками считать с точностью в хулиард знаков. А это та же итерационная процедура.

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

Вот, кстати, решение на Python через эту формулу. Только оно ощутимо медленнее:

Код на Python
from gmpy2 import mpz


class R5:
    def __init__(self, a=0, b=0):
        self.a = mpz(a)
        self.b = mpz(b)

    def __repr__(self):
        return f'R5({self.a}, {self.b})'

    def __str__(self):
        if self.a and self.b:
            return f'({self.a}{self.b:+}√5)'
        elif self.b:
            return f'{self.b}√5'
        else:
            return f'{self.a}'

    def __add__(x, y):
        return R5(x.a + y.a, x.b + y.b)

    def __sub__(x, y):
        return R5(x.a - y.a, x.b - y.b)

    def __mul__(x, y):
        return R5(x.a * y.a + x.b * y.b * 5, x.a * y.b + x.b * y.a)

    def __pow__(x, power):
        if power == 0:
            return R5(1, 0)
        elif power % 2 == 1:
            return x * (x ** (power - 1))
        else:
            sq = x ** (power // 2)
            return sq * sq

    def __floordiv__(x, n):
        n = mpz(n)
        return R5(x.a // n, x.b // n)


def fib_bine(n):
    return (R5(1, 1) ** n - R5(1, -1) ** n).b // mpz(2) ** n


from time import perf_counter
for i in range(1, 10):
    print(i, fib_bine(i))

N = 100_000_000
st = perf_counter()
y = fib_bine(N)
en = perf_counter()
print(f'{en - st:.4}с')
# 1_000_000_000 — 81.16с

Потому что тут по 4 длинных умножения на каждое удвоение номера числа. В статье же свели до 2 длинных умножений.

За O(1) вы не можете его даже напечатать или скопировать из памяти в память, потому что длина числа O(n).

Действительно. И время выполнения тестов у автора чуть больше, чем О(n) как раз и получается:

Тесты

n =10e8 0,6 секунд

n=10e9 7 ceкунд

n=10e10 73 секунды

Где же тут O(log n) !?

Ну, у автора в посте упомянуты "O(log n) операций над числами Фибоначчи", к нему претензий нет. А вот минимум двое минуснувших (один в карму даже не поленился) – показатель.

На самом деле тут O(n log n) получается суммарно. Грубая оценка O(n log^2 n): O(log n) длинных умножений, каждое из которых O(n log n), через FFT. Но они не все такие длинные же. Там в конце пара умножений длины n, потом пара длины n/2, потом пара n/4... В итоге получаем n log n + n/2 log(n/2) + ... и это все можно граничить сверху 2n log n.

При матричном возведении в степень используется 8 умножений, а это медленно. Плюс про этот метод написано много статей.

Там матрица симметричная, так что можно и всего 6 умножения использовать.

Матрицы можно хорошо оптимизировать под конвейер процессора, плюс векторизация

А есть ли какое-нибудь практическое применение знанию миллиардного числа?

Нет, конечно, практического смысла в этом никакого нет. Просто такое упражнение на оптимизацию кода.

Рассказать о его получении, на собеседовании)

Можно ещё на свидании с девушкой блеснуть такими знаниями))

А теперь надо эти мегабайты-гигабайты как-то красиво визуализировать!

Издать в бумажном виде ))

Была похожая книга, только там число пи до какого то знака.

на 130й странице в 18й строке 36й знак напечатан с ошибкой. Приносим извинения...

  f3 = std::move(f6);
  f2 = f6 - f4;

Вот тут у вас UB. Нельзя f6 использовать после того, как вы оттуда сделали move.

Еще, в коде вы там используете fib_addone, когда как выше у вас определена неиспользуемая fib_next.

Советую перечитать весь код в статье и причесать его.

Спасибо за замечания! Исправил!

Кстати, раз уж вы с gmp работаете, было бы интересно сравнить ваше решение с встроенным в gmp: mpz_fib_ui

Кажется, Julia быстро считает, с ней можно сравнить

Интересно, что Python не уступает в скорости: 1 млрд - низкие 5 секунд :)

Код Python
import gmpy2
import time

def fib_next(f2, f1):
    return f1, f2 + f1

def fib_double(f3, f2):
    f6 = (f3 + f2 * 2) * f3
    f4 = (f3 * 2 - f2) * f2
    return f6, f6 - f4

def fib_get(N):
    R = N % 2
    N = (N + 1) // 2

    a = gmpy2.mpz(1)
    b = gmpy2.mpz(0)

    i = N.bit_length() - 1
    h = 1

    for i in range(i - 1, -1, -1):
        a, b = fib_double(a, b)
        h *= 2
        if N & (1 << i):
            a, b = fib_next(a, b)
            h += 1

    if R:
        a = a * a + b * b
    else:
        a = (a + b * 2) * a

    return a


if __name__ == "__main__":
    n = int(input("Enter the Fibonacci number: "))
    start_time = time.time()
    answer = fib_get(n)
    end_time = time.time()

    print(f"Calculation time: {end_time - start_time} seconds")
    with open("answer.txt", "w") as f:
        f.write(answer.digits(16))  # Запись числа в 16-ричной системе
    print("Finish")

1 миллиард: 5.22 сек

Hidden text

10 миллиардов: 77.17 сек

Hidden text

И это на ноутбуке с довольно стареньким Core i9 8950HK и TDP 35W.

gmpy2 это Сишный модуль же... так что Питон тут условный :)

Замените f.write(answer.digits(16)) # Запись числа в 16-ричной системе на f.write(str(answer)) # Запись числа в десятичной системе

for i in range(1, 11):
        print(fib_get(i).digits(10))

Выдает
1 1 2 3 5 5 13 21 29 24

(off) Странно, но в статье не видно КДПВ и текста под ней, поэтому не получается через Ctrl-Enter отправить в личку сообщение об ошибке в тексте на заглавной.Очень глаз режет :peace:

Это уже давно так на хабре стало: текст в ленте - это не часть текста из статьи до какого-то якоря, а вообще отдельный текст. В редакторе для него отдельное поле есть. Бесит на самом деле, что при написании статьи надо постоянно не забывать исправлять текст и там и там.

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

Не миллиард чисел, а одно число. Миллиардное.

и принтер починить не могут)

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

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

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

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

А почему страшно? Для 100 миллиардного ~14 мин и ~17 Гб? )

Как минимум, оно может не поместиться в память, вылететь в своп и считаться всю следующую неделю...

Занятно, недавно наткнулся на похожее видео - https://www.youtube.com/watch?v=KzT9I1d-LlQ

интересно, а можно сделать незаконное число Фибоначчи?

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории