Ejemplos 4

Sucesión de Fibonacci

ALGORÍTMICA Y COMPLEJIDAD
Grados en Ing. Informática y Matemáticas
Universidad de Cantabria
Camilo Palazuelos Calderón

Definimos la función Fibonacci1, que devuelve el $n$-ésimo término de la sucesión de Fibonacci, recibido el entero no negativo $n$ como parámetro.

In [ ]:
def Fibonacci1(n):
    if n <= 1:
        return n
    else:
        return Fibonacci1(n - 1) + Fibonacci1(n - 2)

[Fibonacci1(n) for n in range(10)]
Out[ ]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Evaluamos el coste temporal empírico de Fibonacci1 con $n$ desde $4$ hasta $40$ (de $4$ en $4$).

In [ ]:
import time

m = 40
T = [0]

for n in range(m // 10, m + 1, m // 10):
    t = time.time()
    Fibonacci1(n)
    T.append(time.time() - t)
    #print(f'Iteración {n // (m // 10)} de 10')

Dibujamos una gráfica con el tiempo de ejecución $T(n)$ de Fibonacci1 en función de $n$.

In [ ]:
import matplotlib.pyplot as plt
import numpy as np

with plt.style.context('ggplot'):
    plt.plot(np.array(T), label = '$T(n) \in O(\phi^n)$')
    plt.xticks(
        [x     for x in range(0, 11, 2)],
        [x * 4 for x in range(0, 11, 2)])
    plt.xlabel(        '$n$', fontsize = 12)
    plt.ylabel('$T(n)$ en s', fontsize = 12)
    plt.title('Complejidad temporal')
    plt.legend()

plt.show()
No description has been provided for this image

Definimos las funciones Fibonacci2 y Fibonacci3, las versiones memoizadas recursiva e iterativa, respectivamente, de Fibonacci1.

In [ ]:
def Fibonacci2(n):
    return TopDown(n, [None for _ in range(n + 1)])

def TopDown(n, A):
    if n <= 1:
        return n
    else:
        if A[n] is None:
            A[n] = TopDown(n - 1, A) + TopDown(n - 2, A)
        return A[n]

[Fibonacci2(n) for n in range(10)]
Out[ ]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
In [ ]:
def Fibonacci3(n):
    return BottomUp(n, [None for _ in range(n + 1)])

def BottomUp(n, A):
    for i in range(min(2, n + 1)):
        A[i] = i
    for i in range(2, n + 1):
        A[i] = A[i - 1] + A[i - 2]
    return A[n]

[Fibonacci3(n) for n in range(10)]
Out[ ]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Vamos un paso más allá y definimos la función Fibonacci4 memoizando solo lo necesario para calcular el $i$-ésimo término de la sucesión (para todo $i$ tal que $0 \le i \le n$): los términos $(i - 1)$-ésimo (actual) y $(i - 2)$-ésimo (previo).

In [ ]:
def Fibonacci4(n):
    act, pre = 0, 1
    for _ in range(1, n + 1):
        act, pre = act + pre, act
    return act

[Fibonacci4(n) for n in range(10)]
Out[ ]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Evaluamos el coste temporal empírico de Fibonacci4 con $n$ desde $1 \cdot 10^5$ hasta $10 \cdot 10^5$ (de $10^5$ en $10^5$).

In [ ]:
m = 1000000
T = [0]

for n in range(m // 10, m + 1, m // 10):
    t = time.time()
    Fibonacci4(n)
    T.append(time.time() - t)
    #print(f'Iteración {n // (m // 10)} de 10')

Dibujamos una gráfica con el tiempo de ejecución $T(n)$ de Fibonacci4 en función de $n$.

In [ ]:
with plt.style.context('ggplot'):
    plt.plot(np.array(T), label = '$T(n) \in O(n^2)$')
    plt.xticks(
        [    x               for x in range(0, 11, 2)],
        [f'${x} \cdot 10^5$' for x in range(0, 11, 2)])
    plt.xlabel(        '$n$', fontsize = 12)
    plt.ylabel('$T(n)$ en s', fontsize = 12)
    plt.title('Complejidad temporal')
    plt.legend()

plt.show()
No description has been provided for this image

Nota: Fibonacci4 realiza $n$ sumas de enteros no negativos, y el coste temporal de cada una de estas es $O(m)$, donde $m$ es el número de dígitos de los enteros no negativos que sumar. El número de dígitos del $n$-ésimo número de Fibonacci es asintótico a $n \log_{10} \varphi$, donde $\varphi$ es el número áureo, por lo que el coste temporal de Fibonacci4 es $n \cdot O(n \log_{10} \varphi) = O(n^2)$.