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