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