Devoir surveillé n˚3

publicité
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
Téléchargement