Correction Code nécessaire à la compilation : let bs ="\\" let nl = "\n" let appliquer = List.map (* affichage d'un noeud *) let (noeud_vers_ch : int -> string) = function n -> "fib(" ^ (string_of_int n) ^ ")" Étude expérimentale de la complexité de la fonction de Fibonacci (24 pt) L'objectif de ce problème est d'étudier le nombre d'appels récursifs eectués lors du calcul des nombres de Fibonacci. C'est un exemple classique qui montre qu'une fonction mal écrite peu engendrer un nombre gigantesque d'appels récursifs inutiles. Pour le voir, nous allons demander à la fonction de Fibonacci de construire l'arbre de ses appels récursifs. Dans la seconde partie du problème nous acherons cet arbre et dans la troisième partie nous écrirons une version plus ecace de la fonction de Fibonacci. Première partie du problème (8 pt) 1 Commençons par dénir la fonction de Fibonacci Q1. (1 pt) Donnez la réalisation de la fonction b qui prend en paramètre un entier n et retourne le n ieme nombre de Fibonacci. Prol b : N N Sémantique : b(n) est le n ieme terme de la suite de Fibonacci (u n ) n N dénie par u 0 = 1 u 1 = 1 u n = u n 1 + u n 2 Dénition récursive de la fonction par des équations (1) b( 0 ) = 1 (2) b( 1 ) = 1 (3) b( n ) = b(n 1) + b(n 2) pour n 2
Correction let rec (fib : int -> int) = function 0 -> 1 1 -> 1 n -> fib(n-1) + fib(n-2) Q2. (1 pt) Rédigez la preuve de terminaison de la fonction b. TERMINAISON preuve : (i) On dénit la fonction Mesure ( n ) def = n Justions que la mesure choisie retourne des valeurs dans N: n N (ii) Montrons que la mesure décroit strictement à chaque appel récursif. Pour (ii), on repère les équations qui comportent des appels récursifs et on prouve la décroissance de la mesure pour chaque appel récursif. Rédigez la preuve de terminaison ici (2) b(n) appelle b(n 1) Mesure (n) = n? < Mesure (n 1) ok < = n 1 (2) b(n) appelle aussi b(n 2) Mesure (n) = n? < Mesure (n 2) ok < = n 2 Q3. (.75 pt) Indiquez les appels récursifs engendrés par b(4) sous la forme d'un arbre. Utilisez l'espace libre pour prolonger les branches de l'arbre si nécessaire. b(5) appelle appelle b(4) b(3) appelle appelle appelle appelle b(3) b(2) b(2) b(1)
2 Dénition de l'arbre des appels récursifs Les appels récursifs engendrés par b(x) pour x 2 seront représentés sous la forme d'un arbre ar(ag, x, ad) où ar signigie Appel Récursif le n ud x indique qu'il s'agit du calcul de b(x) et qu'il a nécessité les appels récursifs rangés dans les arbres ag et ad ag est l'arbre des appels récursifs engendrés par b( x 1 ) ad est l'arbre des appels récursifs engendrés par b( x 2 ) Q4. (0.5 pt) Complétez les remarques précédentes et suivantes, ainsi que la dénition du type Abar (Arbre Binaire d'appels Récursifs). L'arbre des appels récursifs de la fonction de Fibonacci est un arbre binaire puisque la fonction fait deux appels récursifs. Lorsque le calcul de b(x) n'engendre pas d'appel récursif son arbre d'appel est représenté par rd(x) où rd signie Résultat Direct et x indique qu'il s'agit du calcul de b(x). Q5. (1 pt) Donnez la représentation à l'aide des constructeurs rd et ar de l'arbre d'appels de b(0) : rd(0) l'arbre d'appels de b(2) : ar( 2, rd(0) ) puis complétez la dénition de Abar. rd(1), DÉFINITION MATHÉMATIQUE D'UN ENSEMBLE déf Abar = { av(x) x {0, 1} } { ar(ag, x, ad) x N, ag, ad Abar } DÉFINITION INFORMATIQUE D'UN TYPE type nat = int (* >=0 *) type abar = Av RD of nat AR of abar * nat * abar Q6. (1.5 pt) Complétez la dénition de la fonction calcul-de-b qui calcule le terme de la suite de Fibonacci et construit simultanément l'arbre des appels récursifs de la fonction b. Prol calcul-de-b : N N Abar Sémantique : calcul-de-b(n) est un couple constitué du n ieme nombre de Fibonacci et de l'arbre des appels récursifs engendrés par le calcul de u n. Exemples 1. calcul-de-b(0) = ( (1, rd(0) ) 2. calcul-de-b(1) = ( (1, rd(1) )
3. calcul-de-b(2) = ( 2, ar(rd(1), 2, rd(0)) ) let rec (calcul_de_fib : int -> int * abar) = function 0 -> (1, RD(0) ) 1 -> (1, RD(1)) n -> let (r1,a1) = calcul_de_fib(n-1) and (r2,a2) = calcul_de_fib(n-2) in (r1+r2, AR(a1, n, a2) ) 3 Étude de l'arbre des appels récursifs On dispose maintenant d'une fonction calcul-de-b qui construit l'arbre d'appels récursifs de b. On aimerait par exemple compter le nombre de fois où le calcul de b(x) a demandé de calculer b(k). Pour cela on a besoin de compter le nombre d'apparitions d'un n ud dans un Abar. Q7. (1.5 pt) Complétez la fonction nb-occ qui compte le nombre d'apparitions du n ud k dans un Abar. Prol nb-occ : Z Abar N Sémantique : nb-occ(k, a) est le nombre d'occurences du n ud k dans l'arbre d'appel a Dénition récursive de la fonction par des équations (1) nb-occ( k, rd(n) ) = 0 si n k (1 ) nb-occ( k, rd(n) ) = 1 si n = k (2) nb-occ( k, ar(ag, n, ad) ) = nb-occ(k, ag) + nb-occ(k, ad) si n k (2 ) nb-occ( k, ar(ag, n, ad) ) = 1 + nb-occ(k, ag) + nb-occ(k, ad) si n = k bonus de 0.25 pour la factorisation du if ou pour une solution qui rend 1 sans appel récursif dans le cas AR(ag,n,ad) où n=k let rec (nb_occ : int * abar -> nat) = function (k,a) -> match a with Av -> 0 RD(n) -> if n=k then 1 else 0 AR(ag,n,ad) ->
(if n=k then 1 else 0) + nb_occ (k,ag) + nb_occ (k,ad)
Q8. (0.75 pt) a) À l'aide des fonctions calcul-de-b et nb-occ, donnez l'expression Ocaml qui permet de connaître le nombre de fois qu'est demandé le calcul de b(1) dans le calcul de b(121). let (_,a) = calcul_de_fib(121) in nb_occ(1,a) Le tableau suivant donne en fonction de n le nombre d'appels à b(1) dans le calcul de b(n). b) Complétez les premières cases du tableau nombre d'appels à b(1) dans le calcul de b(n) n 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 n 15 20 21 22 23 24 25 26 27 nombre d'appels à b(1) dans le calcul de b(n) 610 6765 10946 17711 28657 46368 75025 121393 196418
Seconde partie du problème (12 pt) 4 Achage de l'arbre des appels récursifs L'objectif de cette partie est de réaliser une fonction d'achage d'un arbre d'appel sous la forme suivante, avec les valeurs des appels à la place des (-). Notre objectif est d'acher un arbre fib(_) / \ fib(_) fib(_) / \ / \ fib(_) fib(_) fib(_) fib(_) / \ / \ / \ fib(_) fib(_) fib(_) fib(_) fib(_) fib(_) / \ fib(_) fib(_) ar(ag, n, ad) sous la forme fib(n) / \ ag ad et d'appliquer le même principe au sous-arbre gauche ag et au sous-arbre droit ad. Pour obtenir ce résultat nous devrons 1. transformer le sous-arbre ag en colonne de ligne colg 2. placer le symbole / centré au dessus de cette colonne colg 3. transformer le sous-arbre ad en colonne de ligne cold 4. placer le symbole \ centré au dessus de cette colonne cold 5. coller les colonnes obtenues aux étapes 2 et 4 (opération notée sur le schéma) 6. placer le titre fib(n) centré au dessous des deux colonnes collées an d'obtenir la colonne correspondant à l'arbre ar(ag, n, ad). Le principe de transformation d'un arbre en colonne est résumé par le schéma ci-après : (les êches indiquent que le titre doit être centré). "fib(n)" "/" sous arbre gauche } {{ } colg "\" sous arbre droit } {{ } cold Pour mettre en uvre ce principe nous allons dénir un type Colonne et des opérations de manipulations des colonnes. On décide représenter une colonne de ligne
"abcdefgh" "ijklmnop" "qrstuvwx" "yz" par une séquence de chaîne de caractère [ "abcdefgh" ; "ijklmnop" ; "qrstuvwx" ; "yz" ] Q9. (0.5 pt) Complétez la dénition du type Colonne. DÉFINITION MATHÉMATIQUE D'UN ENSEMBLE déf Colonne = Séq (Chaîne) DÉFINITION INFORMATIQUE D'UN TYPE type colonne = string list et indiquez l'opérateur qui permet d'ajouter une ligne au dessus d'une colonne :: Justiez votre réponse : ajouter une ligne en haut d'une colonne c'est ajouter une chaîne de caractère à gauche d'une séquence Q10. (1 pt) Utilisez l'opérateur Ocaml ( ˆ ) : Chaîne Chaîne Chaîne pour réaliser la fonction espace spéciée ainsi : Prol espace : Z Chaîne Sémantique : espace(n) est la chaînes de caractère consitutée de n espaces. Propriété n 0, espace(n) = "" Correction Dénition récursive de la fonction par des équations (1) espace( n ) = "" si n 0 (2) espace( n ) = " " ˆ espace(n 1) si n > 0 let rec (espace : int -> string) = function n -> if (n<=0) then "" else " " ^ (espace (n-1)) Q11. (1 pt) Utilisez la fonction length prédénie en Ocaml pour réaliser la fonction largeur. Prol length : Chaîne Séq (Élt) N Sémantique : length(ch) est la longueur de la chaîne ch, c'est-à-dire le nombre de caractères qu'elle contient. Notez que la fonction length s'applique aussi bien aux chaînes de caractère qu'aux séquences.
Prol largeur : Colonne N Sémantique : largeur(col) est la largeur de la colonne. Précisons que les lignes de la colonne n'ont pas nécessairement la même longueur. Algorithme : La largeur d'une colonne est la longueur de sa ligne la plus longue. Dénition récursive de la fonction par des équations (1) largeur( [ ] ) = 0 (2) largeur( ch :: s ) = max(length(ch), largeur(s)) ou bien length(ch) si length(ch) > largeur(s) largeur(s) sinon Correction let rec (largeur : colonne -> int) = function [] -> 0 ch::s -> max (String.length ch) (largeur s) Q12. (1 pt) Complétez la réalisation de la fonction titrer qui permet de placer un titre centré au sommet d'une colonne. Prol titrer : Chaîne Colonne Colonne Sémantique : titrer(titre, col) place le titre centré au haut de la colonne. Exemple titrer "titre", "abcdefghijklmnopqrs" "abcdefghijklmnopqrs" "abcdefghijklmnopqrs" "abcdef" = " titre" "abcdefghijklmnopqrs" "abcdefghijklmnopqrs" "abcdefghijklmnopqrs" "abcdef" Algorithme : Notons lc la largeur de la colonne et lt la taille du titre alors le nombre ne d'espaces qu'on doit ajouter à gauche du titre est : ne = lc lt 2 let (titrer : string * colonne -> colonne) = function (titre,col) -> let lc = largeur col in let lt = String.length titre in let ne = (lc - lt)/2 in ((espace ne) ^ titre) :: col
Q13. (0.5 pt) Donnez la réalisation en Ocaml de la fonction compléter-chaine. Prol compléter-chaine : N Chaîne Chaîne Sémantique : compléter-chaine(l, ch) est une chaîne de caractère de taille l constituée de ch suivie d'espaces Exemples 1. compléter-chaine(5, "ab") = "ab " 2. compléter-chaine(5, "abcde") = "abcde" 3. compléter-chaine(5, "") = " " let (completer_chaine : nat * string -> string) = function (larg,ch) -> let lch = (String.length ch) in ch ^ (espace (larg-lch)) Q14. (1 pt) Complétez la spécication de la fonction appliquer. Prol appliquer : (T 1 T 2 ) Séq (T 1 ) Séq ( T 2 ) Sémantique : appliquer f [e 1 ; e 2 ;... ; e n ] = [f(e 1 ) ; f(e 2 ) ;... ; f(e n )] Exemples On rappelle que (f unction x e) est une fonction qui à x associe la valeur de l'expression e. 1. appliquer (function x 2 x + 1) [0; 1; 2; 3] = [1 ; 3 ; 5 ; 7] 2. appliquer (f unction x (x, x x) ) [0; 3; 2; 5; 1] = [ (0, 0) ; (3, 9) ; (2, 4) ; (5, 25) ; (1, 1) ] 3. appliquer (f unction (x, y) y) [ (0, 0); (3, 9), (2, 4); (5, 25); (1, 1) ] = [0; 9; 4; 25; 1] Ajuster une colonne en ajoutant des espaces en n de lignes On appelle colonne ajustée une colonne dont toutes les lignes ont la même taille. On dénit ColonneAjustée comme l'ensemble des couples constitués d'une colonne et de sa largeur.
Q15. (0.25 pt) Complétez la déntion de type. DÉFINITION MATHÉMATIQUE D'UN ENSEMBLE déf ColonneAjustée = Colonne N DÉFINITION INFORMATIQUE D'UN TYPE type colonne_ajustée = colonne * nat Q16. (1.25 pt) Utilisez les fonctions appliquer et compléter-chaine pour réaliser en Ocaml la fonction ajuster-colonne qui complète les lignes d'une colonne par des espaces en n de ligne an que toutes les lignes aient la même taille. Prol ajuster-colonne : Colonne ColonneAjustée Sémantique : ajuster-colonne(col) est la colonne ajustée correspondant à col Exemple ajuster-colonne "abcdef" "abc" "abcdefgh" = "abcdef " "abc " "abcdefgh", 8 "a" "a " Algorithme : On rappelle qu'une colonne est une séquence de chaîne de caractère et qu'on peut donc utiliser la fonction appliquer avec les colonnes. let (ajuster_colonne : colonne -> colonne_ajustée) = function col -> let l = largeur col in let colaj = appliquer (fun ch -> completer_chaine (l,ch)) col in (colaj, l) Q17. (1.5 pt) Complétez la réalisation de la fonction coller qui prend en paramètre deux colonnes déjà ajustées et qui les colle ligne à ligne. Vous pouvez évidemment utiliser des fonctions écrites précédemment. Prol coller : ColonneAjustée ColonneAjustée Colonne Sémantique : coller(caj 1, caj 2 ) est la colonne ajustée obtenue en collant ligne par ligne la colonne ajustée caj 1 à la colonne ajustée caj 2. Précisons que les colonnes sont ajustées, par contre elles n'ont pas nécessairement le même nombre de ligne.
Exemples Vous remarquerez dans les exemples que les deux colonnes doivent être séparées par un espace. 1. coller 2. coller (( "abcdef" "abcdef" "ab " ( ( "abcdef" "ab ", 6 ), ( ) (, 6, "gh " "ghij" "ghi " "ligne 1" "ligne 2" "ligne 3", 4 )), 7 )) = "abcdef gh " "abcdef ghij" "ab ghi " = "abcdef ligne 1" "ab ligne 2" " ligne 3" Dénition récursive de la fonction par des équations (1) coller( ( [ ], l 1 ), ( [ ], l 2 ) ) = [ ] (2) coller( (ch 1 :: s 1, l 1 ), (ch 2 :: s 2, l 2 ) ) = (ch 1 ˆ " " ˆ ch 2 ) :: coller((s 1, l 1 ), (s 2, l 2 )) (3) coller( ([ ], l 1 ), (ch 2 :: s 2, l 2 ) ) = (espace(l 1 ) ˆ " " ˆ ch 2 ) :: coller(([ ], l 1 ), (s 2, l 2 )) (4) coller( (ch 1 :: s 1, l 1 ), (ch 2 :: s 2, l 2 ) ) = (ch 1 ˆ " " ˆ espace(l 2 )) :: coller((s 1, l 1 ), ([ ], l 2 )) Correction let rec (coller : colonne_ajustée * colonne_ajustée -> colonne) = function ((col1,l1),(col2,l2)) -> match (col1,col2) with ([],[]) -> [] (ch1::s1, ch2::s2) -> (ch1 ^ " " ^ ch2) :: (coller ((s1,l1),(s2,l2))) ([],ch2::s2) -> ((espace l1) ^ " " ^ ch2) :: (coller (([],l1),(s2,l2))) (ch1::s1,[]) -> (ch1 ^ " " ^ (espace l2)) :: (coller ((s1,l1),([],l2))) Q18. (0.5 pt) Complétez la réalisation Ocaml de la fonction coller-colonne. Prol coller-colonne : Colonne Colonne Colonne Sémantique : coller-colonne(col 1, col 2 ) est la colonne obtenue en collant les colonnes col 1 et col 2 après ajustement. Exemple coller-colonne ( "abcdef" "ab ", "ligne 1" "ligne 2" "ligne 3" ) = "abcdef ligne 1" "ab ligne 2" " ligne 3"
let (coller_colonne : colonne * colonne -> colonne) = function (col1,col2) -> coller (ajuster_colonne col1, ajuster_colonne col2) On dispose maintenant de toutes les fonctions nécessaires pour réaliser la fonction arbre-vers-colonne qui applique le principe présenté en Section?? pour transformer un arbre d'appel en colonne. Q19. (1.5 pt) Utilisez les fonctions des questions précédentes pour réaliser la fonction arbre-vers-colonne. Prol arbre-vers-colonne : Abar Colonne Sémantique : arbre-vers-colonne(a) est la colonne correspondant à l'arbre a construite selon le principe de la Section??. Dénition récursive de la fonction par des équations (1) arbre-vers-colonne( rd(n) ) = [titre] où titre = "fib(" ˆ string-of-int(n) ˆ ")" (2) arbre-vers-colonne( ar(ag, n, ad) ) = titrer( titre, coller-colonne(colg, cold) ) où colg = titrer( "/", arbre-vers-colonne(ag) ) où cold = titrer( "\\", arbre-vers-colonne(ag) ) où titre = "fib(" ˆ string-of-int(n) ˆ ")" Correction let rec (arbre_vers_colonne : abar -> colonne) = function RD(n) -> [ noeud_vers_ch n ] AR(ag,n,ad) -> let colg = arbre_vers_colonne ag in let cold = arbre_vers_colonne ad in let colgt = titrer ("/",colg) in let coldt = titrer (bs,cold) in let colgd = coller_colonne (colgt, coldt) in titrer (noeud_vers_ch n, colgd) Q20. (0.75 pt) Complétez la réalisation Ocaml de la fonction colonne-vers-chaîne.
Prol colonne-vers-chaîne : Colonne Chaîne Sémantique : colonne-vers-chaîne(col) est la chaîne de caractère construite par concaténation des lignes de la colonne en introduisant un saut de ligne entre les lignes. Algorithme : On obtient un saut de lignes en ajoutant le caractère \n en n de ligne. let rec (colonne_vers_chaine : colonne -> string) = function [] -> "" ch::s -> ch ^ nl ^ (colonne_vers_chaine s) Q21. (1 pt) Dénissez une fonction qui transforme un arbre en chaîne de caractère et donnez ensuite l'expression Ocaml qui permet d'acher à l'écran la chaîne de caractère qui correspond à l'arbre des appels de b(121). Prol arbre-vers-chaîne : Abar Chaîne Sémantique : arbre-vers-chaîne(a) est la chaînes de caractère correspondant à la mise en colonne de l'arbre a let rec (arbre_vers_chaine : abar -> string) = function a -> colonne_vers_chaine (arbre_vers_colonne a) (* let (r,a) = calcul_de_fib(121) in print_string (arbre_vers_chaine(a)) *)
Troisième partie du problème (4 pt) 5 Compter les appels engendrés par b(n) sans construire l'arbre d'appels L'arbre des appels est très utile pour comprendre comment se déroule l'évaluation de la fonction b ; en revanche il n'est pas nécessaire si on souhaite juste compter le nombre total d'appels récursifs engendrés par b(n). Pour eectuer ce décompte nous allons dénir la fonction nb-total-appel-de-b. Q22. (1.5 pt) Complétez la dénition de la fonction nb-total-appel-de-b. Prol nb-total-appel-de-b : N N N Sémantique : nb-total-appel-de-b(n) = (u n, nb) où u n est le n ieme terme de la suite de Fibonacci et nb le nombre total d'appels engendrés par l'appel à b(n) Dénition récursive de la fonction par des équations (1) nb-total-appel-de-b( 0 ) = (1, 1) (2) nb-total-appel-de-b( 0 ) = (1, 1) (3) nb-total-appel-de-b( 0 ) = (u n 1 + u n 2, nb 1 + nb 2 ) si où (u n 1, nb 1 ) = nb-total-appel-de-b(n 1) où (u n 2, nb 2 ) = nb-total-appel-de-b(n 2) n 2 Correction let rec (nb_appel_de_fib : nat -> nat * nat) = function 0 -> (1,1) 1 -> (1,1) n -> let (u_n_1, nb1) = nb_appel_de_fib(n-1) in let (u_n_2, nb2) = nb_appel_de_fib(n-2) in (u_n_2 + u_n_1, nb1 + nb_2) Coïncidence : D'après les équations récursives que pouvez-vous dire du nombre d'appels qu'engendre l'appel b(n)? Justiez votre réponse : C'est exactement le nombre de Fibonnacci puisque la fonction nb-total-appel-de-b retourne un couple dont les deux membres sont égaux d'après les équations). 6 Une version plus ecace de Fibonacci Q23. (2.5 pt) Complétez la dénition de la fonction b2 qui calcule les termes de la suite de Fibonacci sans faire de double appel récursif et montrez la terminaison de la fonction b2.
Prol b2 : N N N Sémantique : b2(n) = (u n, u n+1 ) où u n et u n+1 sont les n ieme et n + 1 ieme termes de la suite de Fibonacci. Dénition récursive de la fonction par des équations (1) b2(0) = (1, 1) (2) b2(n) = (u n, u n + u n 1 ) où (u n 1, u n ) = b2(n 1) let rec (fib2 : int -> int * int) = function 0 -> (1,1) n -> let (u_n_1,u_n) = fib2(n-1) in (u_n, u_n + u_n_1) TERMINAISON On dénit la fonction Mesure ( n ) def = n preuve : (i) Justions que la mesure choisie retourne des valeurs dans N: n N (ii) Montrons que la mesure décroit strictement à chaque appel récursif. Pour (ii), on repère les équations qui comportent des appels récursifs et on prouve la décroissance de la mesure pour chaque appel récursif. (2) f ib2(n) appelle f ib2(n 1) Mesure (n) = n? < Mesure (n 1) ok < = n 1