Почему сложность вычисления ряда Фибоначчи 2^n, а не n^2?

Я пытаюсь найти сложность ряда Фибоначчи, используя дерево рекурсии, и пришел к выводу height of tree = O(n) худший случай, cost of each level = cnотсюда complexity = n*n=n^2

Как это так O(2^n)?

9 ответов

Решение

Сложность наивного рекурсивного фибоначчи действительно равна 2ⁿ.

T(n) = T(n-1) + T(n-2) = T(n-2) + T(n-3) + T(n-3) + T(n-4) = 
= T(n-3) + T(n-4) + T(n-4) + T(n-5) + T(n-4) + T(n-5) + T(n-5) + T(n-6) = ...

На каждом этапе вы звоните T в два раза, тем самым обеспечит возможный асимптотический барьер:
T(n) = 2⋅2⋅...⋅2 = 2ⁿ

Бонус: лучшая теоретическая реализация Фибоначчи - это на самом деле близкая формула, использующая золотое сечение:

Fib(n) = (φⁿ – (–φ)⁻ⁿ)/sqrt(5) [where φ is the golden ratio]

(Тем не менее, он страдает от ошибок точности в реальной жизни из-за арифметики с плавающей запятой, которые не являются точными)

Дерево рекурсии для fib (n) будет выглядеть примерно так:

                              n       
                           /     \
                          n-1    n-2      --------- maximum 2^1 additions
                         /  \    /   \
                       n-2   n-3 n-3 n-4  -------- maximum 2^2 additions
                      /   \           
                     n-3 n-4              -------- maximum 2^3 additions         
                                                ........
                                          -------- maximum 2^(n-1) additions  
  1. Используя n-1 в 2 ^ (n-1), так как для fib(5) мы в конечном итоге перейдем к fib (1)
  2. Количество внутренних узлов = Количество листьев - 1 = 2 ^ (n-1) - 1
  3. Количество сложений = Количество внутренних узлов + Количество листьев = (2^1 + 2^2 + 2^3 + ...) + 2^(n-1)
  4. Мы можем заменить число внутренних узлов на 2 ^ (n-1) - 1, потому что оно всегда будет меньше этого значения: = 2^(n-1) - 1 + 2^(n-1) ~ 2^n

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

Интересно, что на самом деле вы можете установить точное количество вызовов, необходимое для вычисления F (n), как 2F (n + 1) - 1, где F (n) - это n-е число Фибоначчи. Мы можем доказать это индуктивно. В качестве базового случая для вычисления F(0) или F (1) нам нужно сделать ровно один вызов функции, который завершается без каких-либо новых вызовов. Допустим, что L(n) - это количество вызовов, необходимых для вычисления F(n). Тогда у нас есть это

L (0) = 1 = 2 * 1 - 1 = 2F (1) - 1 = 2F (0 + 1) - 1

L (1) = 1 = 2 * 1 - 1 = 2F (2) - 1 = 2F (1 + 1) - 1

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

1 + L(n - 1) + L(n - 2)

= 1 + 2F ((n - 1) + 1) - 1 + 2F ((n - 2) + 1) - 1

= 2F (n) + 2F (n - 1) - 1

= 2 (F (n) + F (n - 1)) - 1

= 2 (F (n + 1)) - 1

= 2F (n + 1) - 1

Что завершает индукцию.

На данный момент, вы можете использовать формулу Бине, чтобы показать, что

L(n) = 2 (1 / √5) (((1 + √5) / 2)n - ((1 - √5) / 2)n) - 1

И, таким образом, L(n) = O (((1 + √5) / 2)n). Если мы используем соглашение, которое

φ = (1 + √5) / 2 ≈ 1.6

У нас есть это

L(n) = Θ (φn)

А так как φ < 2, это o (2 n) (используя обозначения little-o).

Интересно, что я выбрал название L(n) для этой серии, потому что эта серия называется числами Леонардо. Помимо его использования здесь, он возникает при анализе алгоритма сглаживания.

Надеюсь это поможет!

t(n)=t(n-1)+t(n-2), который может быть решен методом дерева:

                                  t(n-1)  +  t(n-2)        2^1=2
                                   |         |  
                            t(n-2)+t(n-3)  t(n-3)+t(n-4)   2^2=4  
                                .               .          2^3=8
                                .               .           .
                                .               .           .

аналогично для последнего уровня., 2^ п
общая сложность по времени =>2+4+8+.....2^n после решения вышеупомянутого gp мы получим сложность по времени как O(2^n)

Посмотри на это так. Предположим, сложность расчета F(k), kth Число Фибоначчи по рекурсии не более 2^k за k <= n, Это наша индукционная гипотеза. Тогда сложность расчета F(n + 1) по рекурсии

F(n + 1) = F(n) + F(n - 1)

который имеет сложность 2^n + 2^(n - 1), Обратите внимание, что

2^n + 2^(n - 1) = 2 * 2^n / 2 + 2^n / 2 = 3 * 2^n / 2 <= 2 * 2^n = 2^(n + 1).

По индукции мы показали, что утверждение о F(k) по рекурсии не более 2^k верно.

Сложность ряда Фибоначчи составляет O(F(k)), где F (k) - это k-е число Фибоначчи. Это можно доказать по индукции. Это тривиально для обоснованного случая. И предположим, что для всех k<=n сложность вычисления F (k) равна c*F(k) + o(F(k)), тогда для k = n+1 сложность вычисления F (n + 1)) это c*F(n) + o(F(n)) + c*F(n-1) + o(F(n-1)) = c*(F(n) + F(n-1)) + o(F(n)) + o(F(n-1)) = O(F(n+1)).

Сложность рекурсивных рядов Фибоначчи составляет 2^n:
Это будут рекуррентные соотношения для рекурсивного Фибоначчи

 T(n)=T(n-1)+T(n-2)                  No  of elements  2

Теперь о решении этого отношения с использованием метода подстановки (подстановка значений T(n-1) и T(n-2))

T(n)=T(n-2)+2*T(n-3)+T(n-4)          No  of elements  4=2^2

Снова подставив значения указанного выше термина, получим

T(n)=T(n-3)+3*T(n-4)+3*T(n-5)+T(n-6)  No  of elements  8=2^3

После полного решения проблемы мы получаем

T(n)={T(n-k)+---------+---------}----------------------------->2^k  eq(3) 

Это подразумевает, что максимальное количество рекурсивных вызовов на любом уровне не будет превышать 2^n.
И для всех рекурсивных вызовов в уравнении 3 ϴ(1), поэтому сложность времени будет 2^n* ϴ(1)=2^n

Сложность O(2^n) вычисления числа Фибоначчи применима только к рекурсивному подходу. С небольшим дополнительным пространством вы можете добиться гораздо лучшей производительности с O(n).

public static int fibonacci(int n) throws Exception {

  if (n < 0)
    throws new Exception("Can't be a negative integer")

  if (n <= 1)
    return n;

  int s = 0, s1 = 0, s2 = 1;
  for(int i= 2; i<=n; i++) {
    s = s1 + s2;
    s1 = s2;
    s2 = s;            
  }
  return s;
}

Я не могу устоять перед искушением соединить итеративный алгоритм линейного времени для Fib с рекурсивным экспоненциальным временем: если кто-то читает замечательную небольшую книгу Джона Бентли "Написание эффективных алгоритмов", я считаю, что это простой случай "кэширования": всякий раз, когда Fib (k) вычисляется, хранится в массиве FibCached[k]. Каждый раз, когда вызывается Fib(j), сначала проверьте, кэшируется ли он в FibCached[j]; если да, вернуть значение; если не использовать рекурсию. (Посмотрите на дерево звонков сейчас...)

Другие вопросы по тегам