Récursivité en Maple

publicité
PCSI2
2010/2011
Récursivité
La récursivité est l’analogue informatique de la récurrence. De même que l’on peut définir des objets
par récurrence en mathématique, on peut définir, en informatique, des objets récursivement, c’est-à-dire
en fonction d’eux-même. Une même procédure peut souvent être écrite de façon itérative (i.e. en utilisant
une boucle, avec des tests) ou récursive (i.e. sans utilisation de boucle, la procédure faisant appel à
elle-même pour le calcul de ses valeurs).
1
Un premier exemple
Écrivons une procédure récursive calculant la somme Sn = 1 + 2 + · · · + n. La seule chose à savoir
pour écrire le programme est la valeur initiale (S0 = 0) et la formule de récurrence (Sn = Sn−1 + n), ce
qui permet le calcul sans utilisation de boucle de la façon suivante :
> S:=proc(n)
if n=0 then 0
else S(n-1) + n
fi
end;
L’instruction debug(S) permet de suivre à la trace les entrées et sorties dans la procédure lors de son
appel. Pour supprimer ces affichages, taper undebug(S).
Le danger d’une telle écriture est de créer un appel infini d’une procédure à elle-même : le programme
ne s’arrête alors pas (ou plutôt, il s’arrête lorsqu’il dépasse ses capacités de mémoire). Ici, si on appelle la
procédure avec un entier positif, alors l’algorithme se termine : en effet, la variable est positive et diminue
strictement tant qu’elle n’est pas nulle ; si elle est nulle, le programme renvoie 0. En revanche, l’appel
à S(-1) provoque l’appel à S(-2), puis à S(-3)... on boucle indéfiniment. Il est souvent plus prudent
d’incorporer des tests aux procédures pour éviter ce genre de gags.
Typage des paramètres
Modifier la procédure précédente de façon à tester que n est un entier positif ou nul et retourner un
message d’erreur dans le cas contraire.
Indication : type(n,nonnegative).
Noter que les anglo-saxons appellent « non negative » les nombres positifs (ou nuls) et qu’ils réservent
l’appellation « positive » aux nombres strictement positifs). Une autre façon d’imposer cette condition
sur n est de le faire dans la déclaration de la procédure : S:=proc(n::nonnegint)
2
Débordement ; table de remember
Écrire maintenant une procédure fact calculant récursivement n!. Quel est le problème lors de l’appel
de cette procédure avec l’argument 1000 ? Pourquoi ? Une façon de remédier à ce problème est d’utiliser
l’option remember dans la définition de la procédure : Maple crée ainsi une table (de remember) dans
laquelle sont stockées toutes les valeurs calculées. Avant de commencer quelque calcul que ce soit, Maple
regarde si la valeur qu’il cherche à calculer se trouve dans la table de remember. Si elle y est, il renvoie cette
valeur sans rien calculer ; sinon, il exécute le corps de la procédure. (Ceci est vrai pour toute procédure ; en
particulier pour les fonctions, qui sont des procédures particulières : regardez la table de remember de la
fonction sin grâce à op(4,eval(sin))). Une autre façon de stocker une valeur dans la table de remember
est de la déclarer à l’extérieur du corps de la procédure (par exemple par fact(0):=1). Pour vider la
table de remember, on demande forget(fact) (après avoir chargé la bibliothèque correspondante avec
readlib(forget)).
3
Exponentiation rapide
La méthode naïve pour calculer an (n ∈ N) consiste à utiliser la définition : a0 = 1 et an+1 = a · an .
Utiliser cette relation pour écrire une procédure récursive, puiss, à deux paramètres, a et n, calculant an
1
pour n ∈ N. Bien sûr, on s’interdira d’utiliser la fonction puissance (ˆ) de Maple : on est justement en
train d’en donner une implémentation !
La méthode d’exponentiation rapide consiste à remarquer que, pour tout entier n ∈ N, on a les
relations
a2n = (a2 )n
et
a2n+1 = a · (a2 )n
(autrement dit, selon que n est pair ou impair, on a an = · · · ou an = · · · ). Cette méthode est beaucoup
plus efficace : elle ramène le calcul d’une puissance n-ème à un calcul de puissance dont l’exposant est
deux fois plus petit. Par exemple, pour calculer a1024 , il n’y a plus besoin de 1023 multiplications, mais
uniquement de 10 : on élève a au carré, puis le résultat au carré, puis... ceci 10 fois de suite.
Écrire maintenant une procédure récursive puissrapide à deux paramètres utilisant cette remarque
pour calculer an . Utiliser une variable globale compteur, initialisée à 0 avant d’appeler la procédure, qui
comptera le nombre de multiplications effectuées pour calculer an par cette méthode. Donner également
un majorant de ce nombre en fonction de n.
4
Coefficients binomiaux
Écrire deux procédures récursives calculant les coefficients binomiaux np : la première utilisant la
relation de Pascal, la seconde la relation np = np n−1
p−1 . Commencer par réfléchir à l’initialisation avant
de coder. En utilisant à nouveau une variable globale, comparer le nombre d’opérations nécessaires en
utilisant l’une et l’autre méthode.
5
La suite de Fibonacci
On rappelle la définition de la suite de Fibonacci :
F0 = 0, F1 = 1
et
∀n ∈ N, Fn+2 = Fn+1 + Fn .
Écrire une procédure récursive Fibo calculant Fn . Calculer F10 , F20 , F30 grâce à cette procédure. Est-il
raisonnable d’espérer calculer F40 avant la fin de la séance ?
La raison de cette inefficacité est que, pour calculer Fn , la procédure Fibo fait non pas un mais deux
appels à elle-même, qui eux-même vont faire chacun deux nouveaux appels à la procédure Fibo... Vérifiezle en « traçant » la procédure Fibo (debug(Fibo)).
Autrement dit, le problème vient de la définition de la suite par récurrence : c’est une définition de la
forme
Fn+1 = f (Fn , Fn−1 )
et non
xn+1 = f (xn )
comme sur les exemples précédents. Une façon de contourner le problème est d’utiliser l’option remember,
mais c’est peu élégant car on est obligé de stocker de nombreuses valeurs.
5.1
D’une récurrence double à une récurrence simple
Une façon plus astucieuse est de se ramener à une suite récurrente dont chaque terme dépend uniquement du précédent. C’est le cas de la suite
xn = (Fn , Fn+1 ),
qui vérifie la relation de récurrence xn+1 = f (xn ), où f est la fonction
f:
R2
(u, v)
−→ R2
.
7−→ (v, u + v)
Nous allons donc écrire une procédure récursive, Fibocouple, qui calcule la liste [Fn , Fn+1 ] pour tout
entier n, en utilisant cette relation de récurrence. Ainsi, le résultat de l’appel à Fibocouple(n) sera
une liste, dont les éléments seront respectivement Fn et Fn+1 , que l’on obtiendra par les commandes
Fibocouple(n)[1] et Fibocouple(n)[2]. La relation de récurrence à utiliser est donc
Fibocouple(n) = f(Fibocouple(n-1)).
On code
2
> f:=(u,v)->(v,u+v);
Fibocouple:=proc(n)
if n = 0 then (0,1)
else f(Fibocouple(n-1))
fi
end;
Pour la simplicité d’utilisation, on peut ensuite écrire une procédure Fibo2, qui calculera (non récursivement) Fn en faisant appel à la procédure Fibocouple :
> Fibo2:=proc(n)
Fibocouple(n)[1]
end;
5.2
Utilisation de matrices
Nous
verrons prochainement que les matrices (de taille (2, 2)) sont des tableaux de nombres A =
a b
que l’on peut multiplier par la formule
c d
′
′
a b
a b′
aa + bc′ ab′ + bd′
· ′
=
c d
c d′
ca′ + dc′ cb′ + dd′
(en d’autres termes, le coefficient situé à l’intersection de la i-ème ligne et de la j-ème colonne du produit
des matrices est le « produit terme à terme » de la i-ème ligne de la première matrice par la j-ème colonne
de la seconde matrice. Nous démontrerons que ce produit est associatif (mais pas commutatif), ce qui
nous permet de parler des puissances d’unematrice A. Le rapport avec les nombres de Fibonacci est le
1 1
suivant : considérons la matrice A =
et ses puissances. Notons an , bn , cn , dn les coefficients de la
1 0
a
bn
. La formule de récurrence An+1 = An · A donne
matrice An : An = n
cn dn
an+1
bn+1
=
=
an
an
+ bn
1 0
d’où bn+2 = an+1 = an + bn = bn+1 + bn . Comme on a par ailleurs b1 = 1 et b0 = 0 (car A =
:
0 1
élément neutre pour la multiplication des matrices), la suite (bn )n est la suite de Fibonacci. Il suffit,
pour calculer Fn , de calculer An par la méthode d’exponentiation rapide et d’en extraire le coefficient en
haut à droite. Pour cela, on écrira
– une procédure récursive calculant les puissances d’une matrice (2, 2) par exponentiation rapide.
On commancera par charger la bibliothèque d’algèbre linéaire par la commande with(linalg).
La matrice identité de taille (2, 2) est codée diag(1,1), la matrice A qui nous intéresse est codée
matrix(2,2,[1,1,1,0]) le produit de deux matrices s’obtient grâce à l’opérateur &* (et non *,
pour le différencier du produit commutatif des nombres par exemple). Un produit de matrices n’est
par défaut pas évalué ; il faut le demander explicitement par la commande evalm (on écrira donc
evalm(A&*B) pour obtenir le produit des deux matrices).
– une procédure (non récursive) qui fera appel à la procédure récursive pour calculer une puissance
de matrice puis extraira le coefficient souhaité de cette matrice (le coefficient situé à l’intersection
de la i-ème ligne et de la j-ème colonne de la matrice M est obtenu par la commande M[i,j]).
Noter l’amélioration : dans la première version, complexité exponentielle (le nombre de calculs croît
de façon exponentielle avec n) ; dans la deuxième version, complexité polynomiale (et même linéaire : le
nombre de calculs est proportionnel à n) ; dans la troisième version, complexité logarithmique (pourquoi ?).
0
3
Téléchargement