Dans cette séance, on illustre les notions de complexité par quelques calculs simples, qui sont aussi l’occasion d’introduire quelques principes et algorithmes de base.
Mettre la précision de xcas à 300 chiffres.
Digits:= 300;
Créer une liste de 3001 coefficients flottants \(a_k\).
Attention, rand(1.)
ne renvoie pas de flottants
à précision 300, on passe par une fonction
transcendante pour obtenir des valeurs ayant vraiment
300 chiffres.
a := seq(evalf(log(rand(50)+2)),k=0..3000);
Calculer \(\sum a_k \pi^k\).
p := evalf(pi); sum(a[k]*p^k, k=0..3000);
Essayer d’avoir la méthode la plus rapide.
On peut éviter de calculer indépendamment les puissances \(p^k\) en écrivant
1 2 3 4 5 6 7 8 9 10 | eval2(a,p) := {
local s, k, pk;
s := 0; pk := 1;
for (k := 0; k < length(a); k++)
{
s := s + a[k] * pk;
pk := pk * p;
}
s;
}
|
et même diviser par 2 le nombre de multiplications en adoptant le schéma de Horner
eval3(a,p) := {
local s, k;
s := 0;
for (k := length(a) - 1; k >= 0; k--)
{ s := s * p + a[k]; }
s;
}
Calculons simplement \(x^n\). Par définition, c’est le produit
faisant intervenir \(n-1\) multiplications. Mais si l’on coupe au milieu, on voit que l’on peut écrire selon la parité de \(n\)
où le terme \(x^k\) ne doit évidemment être calculé qu’une fois.
Écrire une fonction pow_naif(a,n)
qui calcule \(a^n\) de manière
récursive naive.
pow_naif(a,n):={
if(n==0) return(1);
return( a*pow_naif(n-1) );
}
Écrire une fonction pow_rec(a,n)
qui calcule \(a^n\)
par exponentiation rapide de manière récursive.
La tester en calculant pow_rec(a,19)
.
Il y a deux solutions à ce problème, selon l’endroit où l’on prend le carré:
pow_rec(a,n):= {
if(n==0) return 1;
if(irem(n,2)) return a*pow_rec(a,iquo(n,2))^2;
return pow_rec(a,n/2)^2;
}
ou bien:
pow_rec(a,n):= {
if(n==0) return 1;
if(irem(n,2)) return a*pow_rec(a^2,iquo(n,2));
return pow_rec(a^2,n/2);
}
Il y a deux solutions à ce problème, selon l’endroit où l’on prend le carré:
def pow_rec(a,n):
if n==0: return 1
r = pow_rec(a^2,n//2)
if n%2:
return r*a
else:
return r
ou bien:
def pow_rec(a,n):
if n==0: return 1
r = pow_rec(a,n//2)^2
if n%2:
return r*a
else:
return r
Écrire la fonction pow_iter(a,n)
qui effectue le même calcul
de manière itérative.
pow_iter(a,n):= {
local(r:=1);
while(n>0) {
if (n%2==1) {r := r*a};
a := a^2;
n := iquo(n,2);
};
return(r);
}
Si l’on ne veut pas calculer a priori la décomposition binaire de \(n\), la solution la plus simple correspond au premier algorithme récursif.
def pow_iter(a,n):
r = 1
while n>0:
if n%2:
r *= a
a = a^2
n = n//2
return r
Exemple à retenir : quel est le coût du calcul des trois derniers chiffres de \(3^{123456789}\) ?
Pour obtenir les trois derniers chiffres, il faut prendre le reste modulo 1000. Et puisque la projection \(\Z\to \Z/1000\Z\) est un morphisme d’anneaux, il faut surtout effectuer tout le calcul modulo 1000 en écrivant:
pow_rec(3%1000,123456789)
et non pas prendre le reste à la fin, ce qui demande de calculer un nombre gigantesque (ce que Xcas ne peut pas faire):
pow_rec(3,123456789) % 1000
Note
à retenir
Pour tout entier \(n\geq 1\), on peut calculer \(a^n\) avec au plus \(2r\) multiplications où \(r=\floor{\log_2(n)+1}\)
de sorte qu’on peut écrire avec seulement 3 multiplications
Écrire une fonction fact(n)
qui calcule la factorielle de \(n\).
Quelle est sa complexité binaire ?
Implanter la variante suivante, qui consiste à faire uniquement des multiplication entre opérandes de même taille en écrivant
Quelle est la complexité ? Comparer les temps d’exécution pour \(200000!\).
x := Pi
et y := 0.3
.y := y * (2 - y * x)
.
Vers quelle valeur la suite converge-t-elle ?Pour calculer numériquement la racine carrée de \(a\), on définit la suite
Écrire une procédure rac(a)
qui effectue dix itérations de cette suite, et
affiche à chaque étape \(n\) les décimales communes à \(x_n\) et
\(x_{n-1}\). On fera le calcul en précision 200.
Quelle est la vitesse de convergence de la suite ? Combien d’itérations faut-il pour obtenir un million de chiffres de \(\sqrt{5}\) ?
Quelle est la théorie qui se cache derrière ces deux exemples ?
On considère la suite classique \(f_0=f_1=1\), \(f_{n+1}=f_n+f_{n-1}\).
Programmer le calcul récursif naïf de la suite. Quelle est sa complexité ? Calculer \(f_{34}\).
Programmer le calcul des \(f_n\) à l’aide d’une fenêtre glissante (on conserve les deux derniers termes en mémoire). Quelle est la complexité ? Calculer \(f_{10^5}\).
En écrivant la récurrence linéaire
et l’expression de \(F_n=A^nF_0\), calculer à nouveau \(f_{10^5}\). Quelle est la complexité du calcul, pourquoi ?
Comparer les temps de calcul de \(f_{2^{25}-1}\) et \(f_{2^{25}}\), commenter.
Calculer la suite des \(x^n \bmod x^2-x-1\). Que trouve-t-on ?
Rappeler comment on obtient l’expression algébrique exacte
et utilisez-la pour calculer \(f_n\).
Quel est le nombre de chiffres de \(f_{f_{f_{f_{f_4}}}}\) ?