Récursivité en Maple
Transcription
Récursivité en Maple
PCSI2 2010/2011 Récursivité : corrigé des exercices 1 Un premier exemple La procédure S avec vérification du type du paramètre : > S:=proc(n::nonnegint) if n=0 then 0 else S(n-1) + n fi end; Un test : > S(-3); Error, (in S) invalid input: S expects its 1st argument, n, to be of type nonnegint, but received -3 2 Débordement ; table de remember La procédure fact sans l’option remember : > fact:=proc(n) if n=0 then 1 else n * fact(n-1) fi end; > fact(500):fact(1000): Error, (in fact) too many levels of recursion Avec l’option remember : > fact:=proc(n) option remember if n=0 then 1 else n * fact(n-1) fi end; > fact(500):fact(1000): (on ne demande pas l’affichage du nombre 1000!, qui comporte 2567 chiffres...). 3 Exponentiation rapide La version « naïve » : > puiss:=proc(a,n) if n=0 then 1 else a * puiss(a,n-1) fi end; Pour calculer an , l’algorithme effectue n − 1 multiplications (sauf si n = 0...). La méthode d’exponentiation rapide : 1 > puissrapide:=proc(a,n) if n = 0 then 1 elif n = 1 then a elif irem(n,2)=0 then puissrap(a*a,n/2) else a * puissrap(a*a,(n-1)/2) fi end; La terminaison de l’algorithme est assurée par le fait que la procédure se ré-appelle elle-même avec un exposant strictement plus petit qu’à l’appel précédent ; l’exposant finira nécessairement par tomber sur 0 ou sur 1, valeurs pour lesquelles la procédure retourne un résultat (en fait, le cas n = 1 n’est pas indispensable à la correction de la procédure, mais il sert à ce que Maple renvoie directement le résultat dans ce cas, plutôt que de calculer a2 puis de renvoyer a ∗ (a2 )0 comme résultat...). Estimation du nombre de multiplications effectuées : si n est compris entre 2p et 2p+1 − 1, la procédure se ré-appelle elle-même p + 1 fois, en effectuant à chaque appel une ou deux multiplications. Cet entier p est la partie entière de log2 n. Le nombre de multiplications effectué est donc majoré par 2 ⌊log2 n⌋ + 1 . Modifions la procédure pour qu’elle calcule en même temps le nombre de multiplications effectuées, au moyen d’une variable globale compteur, initialisée à 0 avant l’appel de la procédure : > puissrapide:=proc(a,n) global compteur; if n = 0 then 1 elif n = 1 then a elif irem(n,2)=0 then compteur := compteur + 1; puissrap(a*a,n/2) else compteur := compteur + 2; a * puissrap(a*a,(n-1)/2) fi end; Regardons le nombre de multiplications nécessaires au calcul de a10 , a100 , a1000 et a10000 : > for k from 1 to 4 do compteur:=0; puissrapide(a,10^k): compteur od; 4 8 14 17 Ainsi, on n’a besoin que de 17 multiplications pour calculer a10000 . 4 Coefficients binomiaux Supposons qu’on ne cherche à calculer les coefficients np qua dans le cas 0 6 p 6 n (les autres cas, avec n et p entiers positifs, donnent 0). La relation doit être utilisée jusqu’à ce que l’on tombe sur le calcul d’un coefficient situé sur le « bord » du triangle, i.e. vérifiant p = 0 ou n = p. Écrivons directement la version avec variable globale pour compter le nombre d’additions nécessaires : > binom1:=proc(n,p) global compteur; if p = 0 or p = n then 1 else compteur := compteur + 1; 2 binom1(n-1,p) + binom1(n-1,p-1) fi end; Testons : > compteur:=0: binom1(20,10); compteur; 184756 184755 On démontre que le nombre d’additions effectuées par cette méthode est égal à np − 1. La deuxième relation de récurrence fait se réappeler la fonction elle-même jusqu’à tomber sur le cas p = 0, qui suffit donc pour l’initialisation : > binom2:=proc(n,p) global compteur; if p=0 then 1 else compteur := compteur + 2; n * binom2(n-1,p-1) / p fi end; (on compte cette fois-ci le nombre de multiplications et divisions effectuées). Testons à nouveau : > compteur:=0: binom2(20,10); compteur; 184756 20 Un gain considérable de temps ! 5 La suite de Fibonacci La « mauvaise » version : > Fibo:=proc(n) if n <= 1 then n else Fibo(n-1) + Fibo(n-2) fi end; Regardons les appels récursifs à la procédure en la traçant : > debug(Fibo): Fibo(4); {--> {--> {--> {--> enter enter enter enter Fibo, Fibo, Fibo, Fibo, args args args args = = = = 4 3 2 1 1 <-- exit Fibo (now in Fibo) = 1} {--> enter Fibo, args = 0 0 <-- exit Fibo (now in Fibo) = 0} 1 3 <-- exit Fibo (now in Fibo) = 1} {--> enter Fibo, args = 1 1 <-- exit Fibo (now in Fibo) = 1} 2 <-- exit Fibo (now in Fibo) = 2} {--> enter Fibo, args = 2 {--> enter Fibo, args = 1 1 <-- exit Fibo (now in Fibo) = 1} {--> enter Fibo, args = 0 0 <-- exit Fibo (now in Fibo) = 0} 1 <-- exit Fibo (now in Fibo) = 1} 3 <-- exit Fibo (now at top level) = 3} 3 On constate que la fonction se réappelle un grand nombre de fois, pour calculer des valeurs qu’elle a déjà calculées (mais oubliées...). Modifions le code de la procédure de façon à calculer le nombre d’appels à la fonction : > Fibo:=proc(n) global compteur; compteur := compteur + 1; if n <= 1 then n else Fibo(n-1) + Fibo(n-2) fi end; Comptons le nombre d’appels à la procédure pour calculer F10 : > compteur:=0: Fibo(10): compteur; 177 5.1 D’une récurrence double à une récurrence simple > f:=(u,v)->(v,u+v); Fibocouple:=proc(n) if n = 0 then (0,1) else f(Fibocouple(n-1)) fi end; > Fibo2:=proc(n) Fibocouple(n)[1] end; Cette fois-ci, 11 appels à la procédure seulement pour calculer F10 ... 4 5.2 Utilisation de matrices La procédure d’exponentiation rapide pour les matrices : > with(linalg): expomat:=proc(A,n) if n = 0 then diag(1,1) elif n=1 then evalm(A) elif irem(n,2)=0 then expomat(evalm(A&*A), n/2) else evalm(A &* expomat(evalm(A&*A), (n-1)/2)) fi end; D’où la troisième version : > Fibo3:=proc(n) expomat(matrix(2,2,[1,1,1,0]),n)[1,2] end; Comparons les temps de calcul pour les trois versions (on ne s’intéresse pas ici au résultat fourni par la procédure). Les durées sont exprimées en secondes. > t:=time(): Fibo(30): time() - t; 3.394 > t:=time(): Fibo2(500): time() - t; 0.002 > t:=time(): Fibo3(10^6): time() - t; 0.447 5