Devoir surveillé n˚3 MPSI option informatique Lycée Clemenceau Mardi 9 juin 2015 Exercice 1 On admettra que la multiplication de deux entiers naturels se fait en temps constant. 1) On s’intéresse dans cette partie au calcul de la puissance k-ième d’un entier naturel n, pour un entier naturel k ≥ 1. a) Dans un premier temps, on utilise un algorithme naı̈f : let rec puissance n k = if k= 1 then n else n * (puissance n (k-1));; val puissance : int -> int -> int Quelle est la complexité de cet algorithme ? Le justifier précisément. b) On utilise ensuite l’algorithme suivant : let rec puissance2 n k = if k >1 then let x = puissance2 n (k/2) in if k mod2 =0 then x * x else x * x * n else n;; val puissance2 : int -> int -> int i) Décrire l’exécution du programme puissance2 sur l’entrée (2,7). ii) Décrire l’exécution du programme puissance2 sur l’entrée (2,8). iii) Démontrer que le nombre d’appels récursifs à l’intérieur du programme puissance2 sur l’entrée (n, k) est au plus de log2 k + 1. En déduire que le programme termine. iv) Justifier que ce programme est correct. v) Évaluer la complexité de ce programme. 2) On s’intéresse ici au problème de déterminer si un entier naturel est une puissance non triviale d’un nombre entier ou non : on dit qu’un nombre entier naturel n est une puissance entière s’il existe deux entiers naturels k et m tous deux > 1 tels que n = mk . a) Écrire la fonction : test_puissance : int -> int -> bool qui prend en entrée les entiers naturels n > 1 et k > 1 et renvoie le booléen true s’il existe un entier naturel m tel que n = mk et false sinon. Note : l’énoncé original mentionne un N qui n’est pas défini. b) i) Soit n un entier naturel. On suppose que n est une puissance entière : Soient k et m deux entiers naturels > 1 tels que n = mk . Justifier que k 6 log2 (n). ii) Écrire la fonction : 1 test_puissance_entiere : int -> bool qui prend en entrée l’entier naturel n > 1 et renvoie le booléen true s’il existe deux entiers naturels k et m tous deux > 1 tels que n = mk et false sinon. iii) Démontrer que la complexité de ce programme est au plus O(n log2 (n) log2 (k)). iv) En déduire la fonction : liste1_puissances_entieres : int -> int list qui prend en entrée l’entier naturel n > 1 et renvoie la liste des entiers naturels compris entre 2 et n qui sont des puissances entières. c) En vous inspirant du crible d’Erathosthène, écrire la fonction : liste2_puissances_entieres : int -> int list qui prend en entrée l’entier naturel n > 1 et renvoie la liste des entiers naturels compris entre 2 et n qui sont des puissances entières. Exercice 2 : parcours en escalier dans un tableau Soit M une matrice de n+1 lignes et de n+1 colonnes (n > 0), telle que mi,j = 2i 3j pour 0 6 i 6 n et 0 6 j 6 n. 1) Écrire une fonction remplir de paramètre n, de remplissage du tableau à deux dimensions représentant la matrice M . 2) Quel est le nombre total de multiplications de votre algorithme ? 3) Écrire la fonction pgcd qui a prend pour arguments une matrice M (telle que mi,j = 2i 3j pour 0 6 i 6 n et 0 6 j 6 n) et quatre indices i, j, k, l dans [[0, n]] , et retourne pour résultat la valeur du plus grand commun diviseur de mi,j et de mk,l . 4) On dispose d’une matrice M telle que mi,j = 2i 3j pour 0 6 i 6 n et 0 6 j 6 n, écrire une fonction pgm qui étant donné un entier p, 1 6 p 6 2n 3n , calcule les indices i, j du plus grand des minorants de p de la forme 2i 3j . On s’efforcera dans cette question de minimiser le nombre de comparaisons en exploitant la remarque qui suit. Remarque : Si sur la ligne i0 le plus grand des minorants de p se trouve en colonne j0 , alors sur la ligne i0 + 1 le plus grand des minorants de p est : – Soit l’élément qui se trouve sur en (i0 + 1, j0 ), si mi0 +1,j0 = 2mi0 ,j0 est inférieur ou égal à p. – Soit l’élément qui se trouve en (i0 + 1, j0 − 1) , si mi0 +1,j0 = 2mi0 ,j0 est strictement supérieur à p, car dans ce cas mi0 +1,j0 −1 = 23 mi0 ,j0 est alors le plus grand des minorants de p sur la ligne i0 + 1. 5) Combien de comparaisons mettant en jeu un élément de la matrice M effectue votre algorithme ? 2 Problème Le but de ce problème est l’étude de la structure de tas (arbre binaire parfait partiellement trié) et de son application au tri de listes d’entiers. L’intérêt de cette structure est double : elle permet non seulement de réduire le nombre d’opérations de comparaison nécessaires au tri d’une liste mais elle peut également être implantée de manière simple et efficace en utilisant une structure linéaire de type liste ou tableau. L’algorithme du tri par tas exploite deux opérations élémentaires sur les tas : l’insertion d’un nouveau noeud et l’extraction de la racine. Nous n’étudierons que l’opération d’insertion. Cette étude sera réalisée en trois étapes : l’étude de l’opération d’insertion dans les structures d’arbre parfait puis de tas, et l’utilisation de la structure de tas dans un algorithme de tri. I) Représentation des arbres binaires Un arbre binaire composé de noeuds étiquetés par des entiers est représenté par le type CaML type arbre = Vide | Noeud of int * arbre * arbre;; Exemple 1 Le terme suivant Noeud ( 7 , ddddddddNoeud ( 2 , ddddddddddddVide , ddddddddddddNoeud ( 11, Vide , Vide ) ) , ddddddddNoeud ( 13, ddddddddddddNoeud ( 5, Vide , Vide ) , ddddddddddddNoeud ( 1, ddddddddddddddddNoeud ( 18, Vide , Vide ) , ddddddddddddddddVide ) ) ) est alors associé à l’arbre binaire représenté graphiquement par : Question 1 : Écrire en CaML une fonction taille de type arbre -> int telle que l’appel ( taille a ) renvoie le nombre de noeuds contenus dans l’arbre a. Cette fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. La profondeur d’un noeud est définie comme le nombre de liaisons entre la racine de l’arbre et ce noeud. La profondeur d’un arbre est égale au maximum des profondeurs de ses noeuds. Un niveau dans l’arbre est composé de tous les noeuds ayant la même profondeur. Dans l’exemple précédent, l’arbre est de profondeur 3 et les différents niveaux contiennent les noeuds suivants : profondeur 0 1 2 3 3 contenu 7 2,13 1, 5,11 18 II) Étude des arbres binaires II.1) Arbres binaires complets Un arbre binaire complet est un arbre binaire dont tous les niveaux sont complets, c’est-à-dire que tous les noeuds d’un même niveau ont deux fils sauf les noeuds du niveau le plus profond qui n’ont aucun fils. Autrement dit, le niveau de profondeur p contient 2p noeuds. Exemple 2 : Question 2 : Calculer le nombre de noeuds d’un arbre binaire complet de profondeur p. II.2 Arbres binaires parfaits Un arbre binaire parfait est un arbre binaire dont tous les niveaux sont complets sauf le niveau le plus profond qui peut être incomplet auquel cas ses noeuds sont alignés à gauche de l’arbre. Exemple 3 : On remarque qu’il s’agit d’un sous-arbre complet étendu par un dernier niveau partiel contenant les noeuds les plus profonds. On remarque également que : si l’on complète la partie droite du niveau partiel d’un arbre parfait, on obtient un arbre complet. Question 3 : Déterminer un encadrement du nombre n de noeuds dans un arbre parfait en fonction de la profondeur p de cet arbre. Question 4 : En déduire la profondeur d’un arbre parfait contenant n éléments. II.3 Numérotation et occurrence des noeuds L’algorithme naı̈f d’insertion d’un noeud dans un arbre parfait repose sur un parcours en largeur de l’arbre pour trouver la position du dernier noeud. Ce parcours coûteux (complexité de l’ordre de O(taillle(arbre)) peut être évité en utilisant une représentation du chemin menant de la racine à la position où doit être inséré le nouveau noeud. L’algorithme d’insertion consiste ensuite à parcourir l’arbre en suivant le chemin jusqu’à atteindre la position puis à insérer le nouveau noeud. Les questions suivantes permettent de construire cette représentation du chemin. 4 II.3.1 Numérotation hiérarchique des noeuds La numérotation hiérarchique des noeuds d’un arbre parfait consiste à les numéroter par un parcours en largeur en partant de la racine (numéro 1) et en parcourant chaque niveau de gauche à droite. Dans les exemples suivants, le numéro de chaque noeud sera noté en-dessous de son étiquette. Exemple 4 Question 5 : Soit un noeud de numéro n dans le niveau de profondeur p, calculer le nombre de noeuds qui se trouvent à sa gauche dans le niveau de profondeur p. Question 6 : En déduire le nombre de noeuds, dans le niveau de profondeur p+1, qui se trouvent à la gauche des fils du noeud de numéro n (n faisant parti du niveau de profondeur p). Question 7 : Soit un noeud de numéro n, calculer les numéros de ses fils gauche et droit. Question 8 : Déduire de la question précédente, le numéro du père du noeud de numéro n. Pour définir plus simplement les opérations de manipulation des noeuds d’un arbre parfait, nous utiliserons systématiquement les numéros des noeuds. II.3.2 Occurrence d’un arbre Pour accéder à un noeud, nous devons représenter le chemin dans l’arbre allant de la racine au noeud. Pour cela, nous étiquetons la liaison entre un noeud et son fils gauche par l’entier O et la liaison entre un noeud et son fils droit par l’entier 1. Nous appelons alors occurrence, ou chemin, du noeud de numéro n la liste des étiquettes suivies pour aller de la racine à ce noeud. Dans l’exemple suivant, l’occurrence du noeud de numéro 11 étiqueté par la valeur 7 est 0,l.l. Exemple 5 5 Question 9 : Écrire en CaML une fonction occurrence de type int -> int list telle que l’appel (occurrence n) renvoie une liste d’entiers (parmi O et 1) représentant le chemin menant de la racine au noeud de numéro n avec n > O. Cette fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. Question 10 : Expliquer la fonction occurrence définie dans la question précédente. Nous utiliserons systématiquement les occurrences des noeuds dans toutes les opérations de manipulation des arbres parfaits. Nous supposons que les occurrences passées en paramètre sont correctes vis-à-vis des arbres passés en paramètre. II.3.3) Application : accès à l’étiquette d’un noeud Question 11 : Écrire en CaML une fonction consulter de type int list -> arbre -> int telle que l’appel (consulter c a) renvoie la valeur de l’étiquette du noeud accessible en suivant l’occurrence c dans l’arbre parfait a. Cette fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. II.4) Insertion d’un noeud Deux cas se présentent lors de l’insertion d’un noeud dans un arbre parfait : – soit l’arbre est complet, auquel cas il faut ajouter un niveau supplémentaire contenant uniquement le nouveau noeud positionné à l’extrême gauche de l’arbre, – soit le dernier niveau de l’arbre est incomplet, auquel cas le nouveau noeud est inséré à l’extrême droite de ce dernier niveau. Le chemin conduisant au nouveau noeud est donné en paramètre de la fonction d’insertion pour éviter un parcours en largeur de l’arbre nécessaire au calcul de la position du nouveau noeud. Pour insérer le nouveau noeud, il faut suivre ce chemin en partant de la racine de l’arbre pour arriver à sa position, puis ajouter ce noeud comme fils du noeud précédent dans le chemin. Question 12 : Écrire en CaML une fonction inserer de type int -> int list -> arbre -> arbre telle que l’appel (inserer v c a) retourne un arbre parfait obtenu en insérant dans l’arbre a le noeud étiqueté par v et d’occurrence c (cette occurrence correspond à la position du noeud situé après le dernier noeud dans l’arbre parfait a). Cette fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. Question 13 : Expliquer la fonction inserer que vous avez proposée pour la question précédente. Question 14 : Calculer une estimation de la complexité de la fonction inserer en fonction de la taille de l’arbre a. Cette estimation ne prendra en compte que le nombre d’appels récursifs effectués. III Étude des tas Lorsqu’il existe une relation d’ordre total entre les étiquettes des noeuds d’un arbre, un arbre est dit partiellement ordonné si les étiquettes contenues dans les noeuds d’un sous-arbre ont toutes une valeur supérieure ou égale à l’étiquette de la racine de celui-ci. On appelle tas un arbre binaire parfait partiellement ordonné. Exemple 6 : 6 Insertion d’un noeud dans un tas Lors de l’insertion d’un noeud, il est nécessaire de préserver l’ordre partiel dans le tas. Pour cela, il faut échanger les étiquettes de certains noeuds dans la branche menant de la racine au nouveau noeud. Ces échanges seront effectués lors du parcours de l’occurrence menant de la racine à la position du nouveau noeud. L’exemple suivant illustre ces échanges lors de l’insertion du noeud étiqueté par la valeur 5 en suivant l’occurrence l,O,l. Exemple 7 Étape n˚ 1 : liaison entre 2 et 4 : pas d’échange car 5 > 2 Étape n˚ 2 : liaison entre 4 et 11 : pas d’échange car 5 > 4. Étape n˚ 3 : liaison entre 5 et le nouveau noeud : échange car 5 < 11. III.1.1) Programmation de l’insertion Question 15 : En modifiant la fonction inserer de la question 12, écrire en CaML une nouvelle fonction inserer tas de type int -> int list -> arbre -> arbre telle que l’appel (inserer tas v c a ) retourne un tas obtenu en insérant dans le tas a le noeud étiqueté par v et d’occurrence c. Cette fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. Question 16 : Expliquer la fonction inserer tas que vous avez proposée pour la question précédente. En particulier, justifier que l’arbre parfait renvoyé est un tas. Question 17 : Calculer une estimation de la complexité de la fonction inserer tas en fonction de la taille de l’arbre a. Cette estimation ne prendra en compte que le nombre de comparaisons effectuées. 7 IV Application au tri par tas La racine d’un tas contient toujours le minimum des étiquettes des noeuds composant le tas. Le tri par tas d’une liste d’entiers est donc composé de deux étapes : – La construction d’un tas contenant les étiquettes de la liste non triée – L’extraction successive des racines du tas pour construire la liste triée. Question 18 : Écrire en CaML une fonction construire de type int list -> arbre telle que l’appel (construire 1) renvoie un tas contenant les mêmes éléments que la liste l. Cette fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. Question 19 : Expliquer la fonction construire que vous avez proposée pour la question précédente. En particulier justifier que l’arbre engendré par construire est un tas. Question 20 : Calculer une estimation de la complexité de la fonction construire en ,fonction de la taille de la liste l. Cette estimation ne prendra en compte que le nombre de comparaisons effectuées. On dispose de plus d’une fonction CaML extraire de type int list -> arbre -> arbre telle que l’appel (extraire c a) renvoie le tas a dans lequel : – le noeud d’occurence c a été enlevé, – son étiquette remplace celle de la racine, – les étiquettes ont été permutées pour préserver la structure de tas. Cette fonction extrait donc le noeud dont l’étiquette est la plus petite en préservant la structure du tas. Le calcul de la fonction extraire se termine quelles que soient les valeurs de ses paramètres. On ne demande pas d’écrire cette fonction, dont on admettra que la complexité est un O(log2 (taillea)). Question 21 : Écrire en CaML une fonction aplatir de type arbre -> int list telle que l’appel (aplatir a) renvoie la liste triée contenant les mêmes éléments que le tas a. Cette,fonction devra être récursive ou faire appel à des fonctions auxiliaires récursives. Question 22 : Calculer une estimation de la complexité de la fonction aplatir en fonction de la taille de l’arbre a. Cette estimation ne prendra en compte que le nombre de comparaisons effectuées. Question 23 : Écrire en CaML une fonction trier de type int list -> int list telle que l’appel ( trier l ) renvoie la liste triée contenant les mêmes éléments que la liste l. Question 24 : Calculer une estimation de la complexité de la fonction trier en fonction de la taille de la liste l. Cette estimation ne prendra en compte que le nombre de comparaisons effectuées. 8