Ver Mensaje Individual
  #13 (permalink)  
Antiguo 07/06/2010, 03:58
Avatar de dggluz
dggluz
 
Fecha de Ingreso: abril-2009
Ubicación: Buenos Aires, Argentina
Mensajes: 525
Antigüedad: 15 años
Puntos: 50
Respuesta: Fibo(1000): códigos y comentarios.

Disculpen por "arruinarles" este divertido desafío, pero me encontré teniendo que ingeniármelas para entender los códigos que posteaban. Me resultó interesantísimo cuando entendí (al menos en parte) qué es lo que hacían. Decidí entonces facilitarles la tarea a los demás navegantes y explicar una solución, para que no tengan que romperse la cabeza (o que sí lo hagan, pero que no se frustren si no lo entienden).
El código que me quedó calcula bien y rápidamente fibo(1000).

ADVERTENCIA: si quieres resolver el desafío completamente por tu cuenta, no sigas leyendo mi comentario.

Gracias a la comunidad de ForosDelWeb en general por las magníficas ideas aportadas.
Si no sabes qué es la Serie de Fibonacci, primero dedícate a averiguar qué es y luego sigue leyendo el post.

Explicación: la serie de Fibonacci, traducida literalmente de la matemática a la computación resulta una función recursiva como la siguiente:

Código Javascript:
Ver original
  1. function fibonacci(n)
  2. {
  3.     if(n==0)
  4.     {
  5.         return 0;
  6.     }
  7.     if(n<=2)
  8.     {
  9.         return 1;
  10.     }
  11.     return fibonacci(n-1)+fibonacci(n-2);
  12. }

El problema con esa solución es que la computadora hace mucho más proceso que el necesario. Y eso no es sólo porque evaluar recursivamente una función requiera más procesamiento que dentro de un bucle, sino porque sencillamente se calcula de más:
Cada vez que se requiere calcular un fibonacci(n) con n>2, deben calcularse dos números de fibonacci y así recursivamente; pero se calculan muchas veces los mismos números. Veamos un ejemplo:
fibonacci(6):
- Debe calcular fibonacci(5) y fibonacci(4).
fibonacci(5):
- Debe calcular fibonacci(4) y fibonacci(3).
fibonacci(4):
- Debe calcular fibonacci(3) y fibonacci(2).
fibonacci(3):
- Debe calcular fibonacci(2) y fibonacci(1).
fibonacci(2) y fibonacci(1):
- Es 1 (f(0)=0, f(1)=1, f(2)=1)

¿Me siguieron? Para calcular el fibonacci(6) hay que calcular una vez el fibonacci(5), 2 veces el fibonacci(4), 3 veces el fibonacci(3), 5 veces el fibonacci(2) y 3 veces el fibonacci(1) ¡14 cálculos! (más la suma final: ¡15!). Si no me creen háganse una prueba de escritorio con lápiz y papel... les quedará un lindo arbolito.

Obviamente el fibonacci(k) siendo k constante siempre será el mismo, por lo cual calcularlo más de una vez es innecesario. Y lo peor es que la situación empera drásticamente al tener que calcular fibonacci(1000). Hay algunas soluciones computacionales a esto, como memoization.
Pero el problema se evita pensando el cálculo desde otra perspectiva: sólo se necesitan dos variables (más una auxiliar) para calcular cualquier número de fibonacci, siendo que estas variables contienen una el fibonacci(n-1) y la otra el fibonacci(n-2), pero sin necesidad de llamar a la función recursivamente para calcularlos, sino calculándolos a partir de fibonacci(1) y fibonacci(2) y guardando siempre los resultados parciales en estas mismas variables (esta idea la tomé de la solución de Caricatos en este post):

Código Javascript:
Ver original
  1. function fiboN(n)
  2. {
  3.     var f1=1;
  4.     var f2=1;
  5.     var aux;
  6.     for(var i=n; i>2; i--)
  7.     {
  8.         aux=f1;
  9.         f1+=f2;
  10.         f2=aux;
  11.     }
  12.     return f1;
  13. }

Esta solución es sensiblemente más veloz (muchísimo más veloz de hecho, sólo calcula n-2 veces para fibonacci(n)) que la anterior. Pero tiene un problema: Javascript (así como la gran mayoría de lenguajes de programación, una buena excepción es Python) no tiene cálculos aritméticos exactos cuando se trata de números muy grandes (o muy pequeños), sino que usan para almacenarlos en memoria una notación exponencial, que si bien es muy eficiente, es inexacta. Por eso, cuando se intentan calcular fibonacci's grandes, los números empiezan a dar resultados erróneos. La solución que encontré a esto es almacenar los números como cadenas de caracteres. Para ello es necesario definir una suma de cadenas de caracteres como si de números se tratara (el algoritmo que uno utiliza cuando suma "a mano", con arrastre y todo), de modo que la computadora nunca tenga que manejar números grandes (de hecho, nunca mayores a 20). Entonces sí, el código final (muuuuuuuy similar al de Nahuel2k10):

Código Javascript:
Ver original
  1. function fibo(n)
  2. {
  3.     var f1='1';
  4.     var f2='1';
  5.     var aux;
  6.     for(var i=n; i>2; i--)
  7.     {
  8.         aux=f1;
  9.         f1=sumar(f1,f2);
  10.         f2=aux;
  11.     }
  12.     return f1;
  13. }
  14.  
  15. function sumar(a,b)
  16. {
  17.     // Siempre a es mayor o igual que b      %
  18.     var arrastre=false;
  19.     var ret='';
  20.     var suma;
  21.     var dif=a.length-b.length;
  22.     for(var i=a.length-1; i>=0; i--)
  23.     {
  24.         suma=arrastre?1:0;
  25.         suma+=parseInt(a.charAt(i))+parseInt(((i-dif)>=0)?b.charAt(i-dif):'0');
  26.         if(suma>=10)
  27.         {
  28.             arrastre=true;
  29.             suma=suma%10;
  30.         }
  31.         else
  32.         {
  33.             arrastre=false;
  34.         }
  35.         ret=suma+ret;
  36.     }
  37.     if(arrastre)
  38.     {
  39.         ret='1'+ret;
  40.     }
  41.     return ret;
  42. }

¡Suerte!

Última edición por dggluz; 07/06/2010 a las 09:03