On m'a demandé d'écrire une implémentation simple de l'algorithme de Fibonacci, puis de le rendre plus rapide.
Voici ma mise en œuvre initiale
public class Fibonacci {
public static long getFibonacciOf(long n) {
if (n== 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return getFibonacciOf(n-2) + getFibonacciOf(n-1);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner (System.in);
while (true) {
System.out.println("Enter n :");
long n = scanner.nextLong();
if (n >= 0) {
long beginTime = System.currentTimeMillis();
long fibo = getFibonacciOf(n);
long endTime = System.currentTimeMillis();
long delta = endTime - beginTime;
System.out.println("F(" + n + ") = " + fibo + " ... computed in " + delta + " milliseconds");
} else {
break;
}
}
}
}
Comme vous pouvez le voir, j'utilise System.currentTimeMillis () pour obtenir une mesure simple du temps écoulé pendant le calcul de Fibonacci.
Cette implémentation devient rapidement lente et exponentielle comme vous pouvez le voir sur l'image suivante
Donc J'ai une idée d'optimisation simple. Pour mettre les valeurs précédentes dans un HashMap et au lieu de les recalculer à chaque fois, il suffit de les reprendre du HashMap si elles existent. S'ils n'existent pas, nous les mettons ensuite dans le HashMap.
Voici la nouvelle version du code
public class FasterFibonacci {
private static Map<Long, Long> previousValuesHolder;
static {
previousValuesHolder = new HashMap<Long, Long>();
previousValuesHolder.put(Long.valueOf(0), Long.valueOf(0));
previousValuesHolder.put(Long.valueOf(1), Long.valueOf(1));
}
public static long getFibonacciOf(long n) {
if (n== 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
if (previousValuesHolder.containsKey(Long.valueOf(n))) {
return previousValuesHolder.get(n);
} {
long newValue = getFibonacciOf(n-2) + getFibonacciOf(n-1);
previousValuesHolder.put(Long.valueOf(n), Long.valueOf(newValue));
return newValue;
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner (System.in);
while (true) {
System.out.println("Enter n :");
long n = scanner.nextLong();
if (n >= 0) {
long beginTime = System.currentTimeMillis();
long fibo = getFibonacciOf(n);
long endTime = System.currentTimeMillis();
long delta = endTime - beginTime;
System.out.println("F(" + n + ") = " + fibo + " ... computed in " + delta + " milliseconds");
} else {
break;
}
}
}
Ce changement rend l'informatique extrêmement rapide. Je calcule toutes les valeurs de 2 à 103 en un rien de temps et j'obtiens un débordement long à F(104) ( me donne F(104) = -7076989329685730859, ce qui est faux). Je le trouve si vite que ** je me demande s'il y a des erreurs dans mon code (merci de votre vérification et faites le moi savoir s'il vous plaît) **. Veuillez regarder la deuxième photo:
L'implémentation de l'algorithme de mon fibonacci plus rapide est-elle correcte (il me semble que c'est parce qu'elle obtient les mêmes valeurs que la première version, mais comme la première version était trop lente, je ne pouvais pas calculer de plus grandes valeurs avec elle comme F (75))? Quelle autre façon puis-je utiliser pour accélérer la procédure? Ou existe-t-il un meilleur moyen de le rendre plus rapide? Aussi comment puis-je calculer Fibonacci pour de plus grandes valeurs (comme 150, 200) sans obtenir un ** long débordement **? Bien que cela semble rapide, je voudrais pousser les limites. Je me souviens que M. Abrash avait dit ' Le meilleur optimiseur est entre vos deux oreilles', donc je pense que cela peut encore être amélioré. Merci pour l'aide
[Note d'édition:] Bien que cette question aborde l'un des points principaux de ma question, vous pouvez voir ci-dessus que j'ai des problèmes supplémentaires.
Idée: au lieu de recalculer la même valeur plusieurs fois, il vous suffit de stocker la valeur calculée et de les utiliser au fur et à mesure.
f(n)=f(n-1)+f(n-2)
avec f (0) = 0, f (1) = 1. Donc, au moment où vous avez calculé f(n-1), vous pouvez facilement calculer f(n) si vous stockez les valeurs de f(n) et f (n-1).
Prenons d'abord un tableau de Bignums. A [1..200]. Initialisez-les à -1.
fact(n)
{
if(A[n]!=-1) return A[n];
A[0]=0;
A[1]=1;
for i=2 to n
A[i]= addition of A[i],A[i-1];
return A[n]
}
Cela s'exécute en O (n) temps. Vérifiez-le vous-même.
Cette technique est également appelée mémorisation .
La programmation dynamique (généralement appelée DP) est une technique très puissante pour résoudre une classe particulière de problèmes. Cela demande une formulation très élégante de l'approche et une pensée simple et la partie codage est très facile. L'idée est très simple, si vous avez résolu un problème avec l'entrée donnée, puis enregistrez le résultat pour référence future, afin d'éviter de résoudre à nouveau le même problème .. brièvement "Rappelez-vous de votre passé".
Si le problème donné peut être divisé en sous-problèmes plus petits et que ces sous-problèmes plus petits sont à leur tour divisés en des problèmes encore plus petits, et dans ce processus, si vous observez certains over-lappping subproblems
, Alors c'est un gros indice pour DP. De plus, les solutions optimales aux sous-problèmes contribuent à la solution optimale du problème donné (appelé Optimal Substructure Property
).
Il y a deux façons de le faire.
1.) Top-Down: commencez à résoudre le problème donné en le décomposant. Si vous voyez que le problème a déjà été résolu, renvoyez simplement la réponse enregistrée. S'il n'a pas été résolu, résolvez-le et enregistrez la réponse. C'est généralement facile à penser et très intuitif. C'est ce qu'on appelle la mémorisation. (J'ai utilisé cette idée).
2.) De bas en haut: Analysez le problème et voyez l'ordre dans lequel les sous-problèmes sont résolus et commencez à résoudre à partir du sous-problème trivial, vers le problème donné. Dans ce processus, il est garanti que les sous-problèmes sont résolus avant de résoudre le problème. Ceci est appelé Programmation dynamique . (
MinecraftShamrock
a utilisé cette idée)
(Autres façons de procéder)
Regardez notre quête pour obtenir une meilleure solution ne s'arrête pas là. Vous verrez une approche différente-
Si vous savez comment résoudre
recurrence relation
, Alors vous trouverez une solution à cette relation
f(n)=f(n-1)+f(n-2) given f(0)=0,f(1)=1
Vous arriverez à la formule après l'avoir résolue-
f(n)= (1/sqrt(5))((1+sqrt(5))/2)^n - (1/sqrt(5))((1-sqrt(5))/2)^n
qui peut être écrit sous une forme plus compacte
f(n)=floor((((1+sqrt(5))/2)^n) /sqrt(5) + 1/2)
Vous pouvez obtenir la puissance d'un nombre dans les opérations O (logn) . Vous devez apprendre le Exponentiation par quadrature .
[~ # ~] edit [~ # ~] : Il est bon de souligner que cela ne signifie pas nécessairement que le numéro de fibonacci peut être trouvé en O (logn). En fait, le nombre de chiffres dont nous avons besoin pour calculer linéairement les frows. Probablement à cause de la position où j'ai déclaré qu'il semblait prétendre à tort que la factorielle d'un nombre peut être calculée en O(logn) temps. [Bakurui, MinecraftShamrock a commenté cela])
Si vous devez calculer n les nombres de fibonacci très fréquemment, je suggère d'utiliser la réponse d'Amalsom.
Mais si vous voulez calculer un très grand nombre de fibonacci, vous manquerez de mémoire car vous stockez tous des nombres de fibonacci plus petits. Le pseudocode suivant ne conserve que les deux derniers nombres de fibonacci en mémoire, c'est-à-dire qu'il nécessite beaucoup moins de mémoire:
fibonacci(n) {
if n = 0: return 0;
if n = 1: return 1;
a = 0;
b = 1;
for i from 2 to n: {
sum = a + b;
a = b;
b = sum;
}
return b;
}
Analyse
Cela peut calculer des nombres de fibonacci très élevés avec une consommation de mémoire assez faible: Nous avons O (n) temps comme la boucle se répète n-1 fois. La complexité de l'espace est également intéressante: le nième nombre de fibonacci a une longueur de O (n), qui peut facilement être montrée:
Fn <= 2 * Fn-1
Ce qui signifie que le nième nombre de fibonacci est au plus deux fois plus grand que son prédécesseur. Doubler un nombre en binaire équivaut à un seul décalage vers la gauche, ce qui augmente le nombre de bits nécessaires d'une unité. Donc, représenter le nième nombre de fibonacci prend au maximum O(n) espace. Nous avons au plus trois nombres de fibonacci successifs en mémoire ce qui fait O(n) + O(n-1) + O(n-2) = O (n) consommation totale d'espace. Contrairement à l'algorithme de mémorisation conserve toujours les n premiers nombres de fibonacci en mémoire, ce qui fait O(n) + O(n-1) + O(n-2) + ... + O(1) = O (n ^ 2) consommation d'espace.
Alors, quel chemin utiliser?
La seule raison de garder en mémoire tous les nombres de fibonacci inférieurs est si vous avez besoin de nombres de fibonacci très fréquemment. Il s'agit d'équilibrer le temps avec la consommation de mémoire.
Éloignez-vous de la récursion de Fibonacci et utilisez les identités
(F(2n), F(2n-1)) = (F(n)^2 + 2 F(n) F(n-1), F(n)^2+F(n-1)^2)
(F(2n+1), F(2n)) = (F(n+1)^2+F(n)^2, 2 F(n+1) F(n) - F(n)^2)
Cela vous permet de calculer (F (m + 1), F(m)) en termes de (F (k + 1), F(k)) pour k la moitié de la taille de m. Écrit itérativement avec un peu de décalage de bits pour la division par 2, cela devrait vous donner la vitesse théorique O (log n) d'exponentiation par quadrature tout en restant entièrement dans l'arithmétique entière. (Eh bien, les opérations arithmétiques O (log n). Puisque vous travaillerez avec des nombres avec environ n bits, il ne sera pas temps O (log n) une fois que vous serez obligé de passer à une grande bibliothèque d'entiers. Après F (50 ), vous dépasserez le type de données entier, qui ne monte que jusqu'à 2 ^ (31).)
(Toutes mes excuses pour ne pas se souvenir de Java suffisamment bien pour l'implémenter en Java; toute personne qui le souhaite est libre de le modifier.)
Habituellement, il existe 2 façons de calculer le nombre de Fibonacci:
Récursion :
public long getFibonacci(long n) {
if(n <= 1) {
return n;
} else {
return getFibonacci(n - 1) + getFibonacci(n - 2);
}
}
Cette méthode est intuitive et facile à comprendre, alors qu'elle ne réutilise pas le nombre de Fibonacci calculé, la complexité temporelle est d'environ O(2^n)
, mais elle ne stocke pas le résultat calculé, donc elle économise beaucoup d'espace, en fait l'espace la complexité est O(1)
.
Programmation dynamique :
public long getFibonacci(long n) {
long[] f = new long[(int)(n + 1)];
f[0] = 0;
f[1] = 1;
for(int i=2;i<=n;i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[(int)n];
}
Cette mémorisation façon de calculer les nombres de Fibonacci et de les réutiliser lors du calcul du suivant. La complexité temporelle est assez bonne, qui est O(n)
, tandis que la complexité spatiale est O(n)
. Voyons si la complexité de l'espace peut être optimisée ... Puisque f(i)
ne nécessite que f(i - 1)
et f(i - 2)
, il n'est pas nécessaire de stocker tous les nombres de Fibonacci calculés.
L'implémentation la plus efficace est:
public long getFibonacci(long n) {
if(n <= 1) {
return n;
}
long x = 0, y = 1;
long ans;
for(int i=2;i<=n;i++) {
ans = x + y;
x = y;
y = ans;
}
return ans;
}
Avec la complexité temporelle O(n)
, et la complexité spatiale O(1)
.
Ajouté: Comme le nombre de Fibonacci augmente rapidement, long
ne peut gérer que moins de 100 numéros de Fibonacci . En Java, nous pouvons utiliser BigInteger
pour stocker plus de nombres de Fibonacci.
Précalculez un grand nombre de résultats fib(n)
et stockez-les sous forme de table de recherche dans votre algorithme. Bam, "vitesse" gratuite
Maintenant, si vous avez besoin de calculer fib(101)
et que vous avez déjà des fibs 0 à 100 stockés, c'est comme essayer de calculer fib(1)
.
Il y a des chances que ce ne soit pas ce que ces devoirs recherchent, mais c'est une stratégie tout à fait légitime et essentiellement l'idée de mise en cache extraite plus loin de l'exécution de l'algorithme. Si vous savez que vous calculez probablement les 100 premiers fibs souvent et que vous devez le faire vraiment très rapidement, il n'y a rien de plus rapide que O (1). Donc, calculez ces valeurs entièrement hors bande et stockez-les afin qu'elles puissent être consultées plus tard.
Bien sûr, les valeurs de cache lorsque vous les calculez aussi :) Le calcul dupliqué est du gaspillage.
Voici du code avec une approche itérative au lieu de la récursivité.
Exemple de sortie :
Enter n: 5
F(5) = 5 ... computed in 1 milliseconds
Enter n: 50
F(50) = 12586269025 ... computed in 0 milliseconds
Enter n: 500
F(500) = ...4125 ... computed in 2 milliseconds
Enter n: 500
F(500) = ...4125 ... computed in 0 milliseconds
Enter n: 500000
F(500000) = ...453125 ... computed in 5,718 milliseconds
Enter n: 500000
F(500000) = ...453125 ... computed in 0 milliseconds
Certains éléments de résultats sont omis avec ...
pour une meilleure vue.
Extrait de code :
public class CachedFibonacci {
private static Map<BigDecimal, BigDecimal> previousValuesHolder;
static {
previousValuesHolder = new HashMap<>();
previousValuesHolder.put(BigDecimal.ZERO, BigDecimal.ZERO);
previousValuesHolder.put(BigDecimal.ONE, BigDecimal.ONE);
}
public static BigDecimal getFibonacciOf(long number) {
if (0 == number) {
return BigDecimal.ZERO;
} else if (1 == number) {
return BigDecimal.ONE;
} else {
if (previousValuesHolder.containsKey(BigDecimal.valueOf(number))) {
return previousValuesHolder.get(BigDecimal.valueOf(number));
} else {
BigDecimal olderValue = BigDecimal.ONE,
oldValue = BigDecimal.ONE,
newValue = BigDecimal.ONE;
for (int i = 3; i <= number; i++) {
newValue = oldValue.add(olderValue);
olderValue = oldValue;
oldValue = newValue;
}
previousValuesHolder.put(BigDecimal.valueOf(number), newValue);
return newValue;
}
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("Enter n: ");
long inputNumber = scanner.nextLong();
if (inputNumber >= 0) {
long beginTime = System.currentTimeMillis();
BigDecimal fibo = getFibonacciOf(inputNumber);
long endTime = System.currentTimeMillis();
long delta = endTime - beginTime;
System.out.printf("F(%d) = %.0f ... computed in %,d milliseconds\n", inputNumber, fibo, delta);
} else {
System.err.println("You must enter number > 0");
System.out.println("try, enter number again, please:");
break;
}
}
}
}
Cette approche s'exécute beaucoup plus rapidement que la version récursive.
Dans une telle situation, la solution itérative a tendance à être un peu plus rapide, car chaque appel de méthode récursive prend un certain temps de processeur. En principe, il est possible pour un compilateur intelligent d’éviter les appels de méthode récursifs s’ils suivent des modèles simples, mais la plupart des compilateurs ne le font pas. De ce point de vue, une solution itérative est préférable.
Ayant suivi une approche similaire il y a quelque temps, je viens de réaliser qu'il y a une autre optimisation que vous pouvez faire.
Si vous connaissez deux grandes réponses consécutives, vous pouvez l'utiliser comme point de départ. Par exemple, si vous connaissez F (100) et F (101) , puis calculer F (104) est approximativement égal à difficile (*) comme calcul F (4) basé sur F (0) et F (1) .
Calculer de manière itérative est aussi efficace sur le plan du calcul que faire la même chose en utilisant la récursivité mise en cache, mais utilise moins de mémoire.
Après avoir fait quelques sommes, je me suis également rendu compte que, pour tout z < n
:
F(n)=F(z) * F(n-z) + F(z-1) * F(n-z-1)
Si n est impair et que vous choisissez z=(n+1)/2
, cela est réduit à
F (n) = F (z) ^ 2 + F (z-1) ^ 2
Il me semble que vous devriez pouvoir utiliser ceci par une méthode que je n'ai pas encore trouvée, que vous devriez pouvoir utiliser les informations ci-dessus pour trouver F(n) dans le nombre d'opérations égal à:
le nombre de bits dans n doublages (comme ci-dessus) + le nombre de 1
bits dans n ajouts; dans le cas de 104, ce serait (7 bits, 3 '1' bits) = 14 multiplications (quadrature), 10 additions.
(*) en supposant ( l'ajout de deux nombres prend le même temps, peu importe la taille des deux nombres.
Voici un moyen de le faire de manière prouvée dans [~ # ~] o [~ # ~] (log n ) (pendant l'exécution de la boucle log n
fois):
/*
* Fast doubling method
* F(2n) = F(n) * (2*F(n+1) - F(n)).
* F(2n+1) = F(n+1)^2 + F(n)^2.
* Adapted from:
* https://www.nayuki.io/page/fast-fibonacci-algorithms
*/
private static long getFibonacci(int n) {
long a = 0;
long b = 1;
for (int i = 31 - Integer.numberOfLeadingZeros(n); i >= 0; i--) {
long d = a * ((b<<1) - a);
long e = (a*a) + (b*b);
a = d;
b = e;
if (((n >>> i) & 1) != 0) {
long c = a+b;
a = b;
b = c;
}
}
return a;
}
Je suppose ici (comme c'est conventionnel) qu'une opération multiplier/ajouter/quelque soit le temps constant quel que soit le nombre de bits, c'est-à-dire qu'un type de données de longueur fixe sera utilisé.
Cette page explique plusieurs méthodes dont celle-ci est la plus rapide. Je l'ai simplement traduit en utilisant BigInteger
pour plus de lisibilité. Voici la version BigInteger
:
/*
* Fast doubling method.
* F(2n) = F(n) * (2*F(n+1) - F(n)).
* F(2n+1) = F(n+1)^2 + F(n)^2.
* Adapted from:
* http://www.nayuki.io/page/fast-fibonacci-algorithms
*/
private static BigInteger getFibonacci(int n) {
BigInteger a = BigInteger.ZERO;
BigInteger b = BigInteger.ONE;
for (int i = 31 - Integer.numberOfLeadingZeros(n); i >= 0; i--) {
BigInteger d = a.multiply(b.shiftLeft(1).subtract(a));
BigInteger e = a.multiply(a).add(b.multiply(b));
a = d;
b = e;
if (((n >>> i) & 1) != 0) {
BigInteger c = a.add(b);
a = b;
b = c;
}
}
return a;
}