Números de Fibonacci
Los números de Fibonacci se definen mediante la siguiente secuencia
Así, cada número de Fibonacci es la suma de los dos anteriores, produciendo la secuencia
Algoritmo recursivo
Dado un número positivo \( n \), queremos calcular \(F_n \). Una primera aproximación es utilizar el siguiente algoritmo:
1
2
3
4
def fib(n):
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
Un problema que podemos ver en este algoritmo al ejecutarlo es su lentitud, la cual se debe a que repite mucho trabajo. Por ejemplo, si queremos calcular \(F_5\) el árbol de llamadas a la función es el siguiente:
La llamada a fib(5)
llama a fib(4)
y fib(3)
, pero la llamada a fib(4)
también llama a fib(3)
. El valor retornado es el mismo para ambas llamadas
\((F_3 = 2)\), por lo que está repitiendo trabajo cada vez que vuelve a
llamar a la función con el mismo argumento y el algoritmo no tiene idea de que
esto está ocurriendo. Para el caso fib(5)
podemos ver la cantidad de llamadas
de cada función en la siguiente tabla
función | n° llamadas |
---|---|
fib(5) |
1 |
fib(4) |
1 |
fib(3) |
2 |
fib(2) |
3 |
fib(1) |
5 |
fib(0) |
3 |
Eficiencia asintótica del algoritmo recursivo
A medida que \(n\) va creciendo, el algoritmo recursivo es cada vez menos eficiente, ya que las llamadas a una misma función van aumentando. Veremos el orden de crecimiento del tiempo de ejecución analizando la eficiencia asintótica del algoritmo. El tiempo de ejecución está dado por la ecuación de recurrencia
Esta ecuación nos muestra que tiempo de ejecución del algoritmo es la suma de los tiempos de ejecución del algoritmo, evaluados en \(n - 1\) y \(n - 2\) más un tiempo constante que representa la suma de los valores retornados por las llamadas recursivas (línea 4 del algoritmo).
Resolución de la ecuación de recurrencia
Utilizaremos el método de sustitución para resolver la ecuación \(\eqref{i}\). Suponemos como hipótesis inductiva que el tiempo de ejecución es
y sustituimos en la ecuación \(\eqref{i}\) quedando
Elegimos un \(b\) lo suficientemente grande para dominar a \(\Theta(1)\) en \(\eqref{ii.2}\) y satisfacer la desigualdad \(\eqref{ii.3}\), y un \(a\) también suficientemente grande para satisfacer \(\eqref{ii.1}\).
Relación con el número aúreo
La sucesión de Fibonacci puede ser representada como una expresión de forma cerrada conocida como la fórmula de Binet
Donde \(\phi\) es el número aúreo y \(\psi\) su conjugado, definidos por \(\phi = \frac{1 + \sqrt{5}}{2}\) y \(\psi = \frac{1 - \sqrt{5}}{2}\). Se puede ver fácilmente que \(\lvert\psi\rvert < 1\), entonces tenemos que
Esto significa que el aporte de \(\psi\) es menor a \(\frac{1}{2}\) en valor absoluto y por lo tanto \(F_i\) es el entero más cercano a \(\frac{\phi^n}{\sqrt{5}}\), lo que podemos escribir como
Finalmente si reemplazamos \(\eqref{iii}\) en \(\eqref{ii}\) podemos concluir que
Lo que nos da un crecimiento exponencial parta el algoritmo recursivo, mostrando por qué a medida que aumentamos el \(n\), el algoritmo es cada vez más lento. Es necesario entonces que encontremos otra aproximación al problema.
Algoritmo lineal
Una aproximación más eficiente a este problema es la siguiente:
1
2
3
4
5
def fib(n):
a, b = 0, 1
for __ in range(n - 1):
a, b = b, a + b
return b
Este algoritmo parte de los dos valores iniciales y va generando la secuencia a medida que la va recorriendo, llevando el conteo sólo de los dos ultimos números. Como recorre la secuencia solo una vez, también calcula cada número una vez por lo que tiene un tiempo de ejecución del orden de \( \Theta(n) \), el cual es muchísimo menor que el del algoritmo recursivo, descrito en la sección previa.
Comparación empírica del tiempo de ejecución de ambos algoritmos
Finalmente se muestra una tabla no exhaustiva que muestra el tiempo en segundos que se demora en calcular algunos números de Fibonacci para ambos algoritmos:
n | Recursivo | Lineal |
---|---|---|
5 | \( 1.22 \times 10^{-5}\) | \( 9.78 \times 10^{-6}\) |
10 | \( 4.50 \times 10^{-5}\) | \( 1.03 \times 10^{-5}\) |
15 | \( 4.03 \times 10^{-4}\) | \( 1.12 \times 10^{-5}\) |
20 | \( 4.56 \times 10^{-3}\) | \( 1.32 \times 10^{-5}\) |
25 | \( 5.64 \times 10^{-2}\) | \( 1.17 \times 10^{-5}\) |
30 | \( 5.98 \times 10^{-1}\) | \( 1.37 \times 10^{-5}\) |
35 | \( 6.48 \times 10^0\) | \( 1.32 \times 10^{-5}\) |
40 | \( 7.66 \times 10^1\) | \( 1.37 \times 10^{-5}\) |
45 | \( 9.23 \times 10^2\) | \( 1.42 \times 10^{-5}\) |
50 | \( 1.07 \times 10^4\) | \( 1.47 \times 10^{-5}\) |