Votre opinion sur le cours de ce matin : | Votre opinion sur le TD de la semaine dernière : |
On rappelle qu'une file est une structure de données dans laquelle on peut mettre des éléments et en prendre, de sorte que les éléments pris le sont dans l'ordre dans lequel ils ont été mis (premier entré premier sorti). On rappelle aussi qu'un tas est un arbre dans lequel les clefs croissent le long des branches, c'est-à-dire que la clef d'un nœud est toujours inférieure ou égale à la clef de chacun de ses fils. Ici, une clef est un entier, qui représente une priorité.
L'objectif de ce TD est d'implémenter les tas binomiaux qui sont une structure particulière permettant de programmer efficacement des files de priorité. Celles-ci sont une variante des files dans lesquelles les éléments sont entrés avec une priorité (qui est un entier) et sortent par ordre de priorité : les éléments de plus petite priorité sortent en premier, et dans le cas où la file contient plusieurs éléments de priorité minimale, l'élément entré en premier sort en premier.
Le TD devra être réalisé en OCaml. On pourra utiliser soit utiliser Eclipse, soit un éditeur de texte classique (par exemple Emacs).
Nous allons commencer par définir un module BinomialTree
permettant de définir des tas fondés sur des arbres particuliers,
appelés arbres binomiaux. Dans ces arbres un nœud peut avoir un
nombre quelconque de fils, et l'ordre des fils importe.
'a tree
d'arbre dont les nœuds sont
étiquetés par une priorité qui est un entier, et
une valeur de type quelconque 'a
. Un nœud peut
avoir un nombre quelconque de fils. La structure de données devra être
immuable (on ne modifiera pas la priorité ou la valeur d'un nœud).
node : int -> 'a -> 'a tree
qui prend
en argument une priorité et une valeur et renvoie un arbre à un nœud.
L'ensemble des arbres binomiaux de rang k est défini inductivement par :
link
qui prend deux arbres binomiaux
de même rang k et crée un nouvel arbre binomial de rang k+1
selon la méthode précédente.
size
qui calcule le nombre de nœuds d'un arbre. Il
pourra être utile de simultanément définir une fonction
auxiliaire size_list
qui calcule la somme des tailles d'une
liste d'arbres en utilisant la structure de code suivante :
let rec size t = ... and size_list = function | [] -> ... | t :: l -> ...Vérifier la propriété précédente sur quelques exemples :
let () = let zero = node 0 "a" in let one = link zero zero in let two = link one one in assert (size two = 4)
rank
qui calcule le
rang d'un arbre et la tester :
let () = let zero = node 0 "a" in let one = link zero zero in let two = link one one in assert (rank two = 2)
'a tree
devra être abstrait (on veut pouvoir modifier
son implémentation par la suite) et déclarer les fonctions ci-dessus
(node
, link
et rank
).
link
de sorte à lever une exception
lorsque les deux arbres donnés en argument n'ont pas le même rang (c'est
le seul cas dans lequel la fonction a du sens).
rank
étant maintenant couramment utilisée, on
souhaiterait qu'elle soit effectuée le plus efficacement
possible. Modifier la structure d'arbre 'a tree
afin de
maintenir le rang et modifier l'implémentation de rank
pour
qu'elle soit calculée en temps constant. On s'efforcera de conserver la
même interface (mli) : la déclaration de 'a
tree
comme type abstrait a permis d'améliorer l'implémentation
sans changer la signature du module.
link
de sorte que si ses arguments
sont des tas (i.e. la priorité d'un nœud est toujours plus petite que
celle de l'un de ses fils), le résultat soit encore un tas, quitte à
échanger le rôle des deux arguments.
Déposer les fichier BinomialTree.ml et BinomialTree.mli :
Afin de s'exercer pour la partie suivante, nous allons implémenter dans un
autre module Binary
quelques manipulations simples d'entiers
binaires codés comme des listes d'entiers : un entier binaire sera codé
par la liste croissante des indices des bits à 1 dans son écriture
binaire. Par exemple, l'entier 19, dont l'écriture binaire
est 10011, sera codé par la liste [0;1;4]
. On
notera binary = int list
le type des entiers binaires codés
de cette façon. Le module aura l'interface Binary.mli suivante :
type binary val to_int : binary -> int val of_int : int -> binary val add_bit : binary -> int -> binary val add : binary -> binary -> binaryOn pourra partir du fichier Binary.ml suivant :
type binary = int list let to_int b = (* à compléter *) assert false let of_int n = (* à compléter *) assert false let add_bit b k = (* à compléter *) assert false let add b1 b2 = (* à compléter *) assert false
to_int : binary -> int
et of_int : int -> binary
et testez-les :
let () = assert (to_int [0;1;4] = 19); assert (of_int 19 = [0;1;4])On peut calculer 2k via l'expression
1 lsl
k
(lsl
signifie logical shift left).
On peut obtenir le dernier bit d'un entier positif n en
calculant n mod 2
(qui vaudra 0
ou 1
). On peut « effacer » le dernier bit d'un
entier n en calculant n/2
.
Indice: vous pourrez écrire une fonction auxiliaire
of_int_aux
, de type int -> int -> binary
,
telle que of_int_aux
convertit l'entier
2kn en une liste.
Intuitivement, l'entier n
est un entier dont on a
déjà « effacé » plusieurs bits (en le divisant
par 2, comme indiqué plus haut), et l'entier k
indique justement combien de bits ont déjà
été « effacés ».
add_bit : binary -> int -> binary
qui calcule le
résultat de la somme n+2k en binaire. Testez-la :
let () = assert (add_bit [0;1;4] 3 = [0;1;3;4]); assert (add_bit [0;1;4] 5 = [0;1;4;5]); assert (add_bit [0;1;4] 0 = [2;4])
add : binary -> binary -> binary
qui
implémente l'addition générale et la tester sur quelques
exemples :
let () = let add_int x y = to_int (add (of_int x) (of_int y)) in assert (add_int 19 33 = 52); assert (add_int 41 29 = 70); assert (add_int 33 85 = 118)
Déposer le fichier Binary.ml :
Nous allons maintenant implémenter, dans un nouveau
module BinomialHeap
, les tas binomiaux. Ce sont des files
permettant de stocker efficacement des (multi)ensembles de paires priorité
/ valeur, d'en ajouter de nouvelles, et d'extraire une valeur de priorité
minimale rapidement. L'idée pour stocker
n valeurs va être la suivante. Supposons que les indices de bits
à 1 dans l'écriture binaire de n soient i1,
i2, ..., ik, c'est-à-dire que n =
∑p=1k2ip. Nous allons
découper l'ensemble des valeurs en sous-ensembles de taille
2ip, chacun de ces ensembles étant stocké dans
les nœuds d'un arbre binomial de rang ip. Ainsi, un
tas binomial sera une liste d'arbres binomiaux de rangs respectifs i1,
i2, ..., ik avec i1 <
i2 < ... < ip. Autrement dit, un tas
binomial est
une liste d'arbres binomiaux tels que
'a heap
des tas binomiaux contenant
des valeurs de type 'a
.add_tree : 'a heap -> 'a tree -> 'a
heap
qui ajoute un arbre binomial supposé croissant (un tas) à une
file. On veillera à maintenir les propriétés précédentes et l'on pourra
s'inspirer de la fonction add_bit
écrite précédemment.insert : 'a heap -> int -> 'a -> 'a
heap
qui ajoute un nouvel élément de priorité et de valeur données
à une file.find_min : 'a heap -> 'a
qui renvoie
l'élément de priorité minimale le plus ancien d'une file en temps
linéaire par rapport au nombre d'arbres. (On ne demande pas ici
d'extraire l'élément de la file; seulement de le trouver.)
Il pourra être utile d'ajouter
au module BinomialTree des fonctions permettant
d'obtenir la clé
et la valeur de la racine d'un arbre.
let () = let h = empty in let h = insert h 5 "a" in assert (find_min h = "a"); let h = insert h 8 "b" in assert (find_min h = "a"); let h = insert h 2 "c" in assert (find_min h = "c"); let h = insert h 2 "d" in assert (find_min h = "c"); let h = insert h 2 "e" in assert (find_min h = "c")
Déposer les fichier BinomialHeap.ml et BinomialHeap.mli :
Les questions suivantes sont optionnelles. Elles permettent de voir l'utilisation que l'on peut faire des files de priorité.
Nous allons commencer par implémenter quelques autres fonctions utiles sur les files binomiales.
merge : 'a heap -> 'a heap -> 'a heap
qui fusionne deux files, en vous inspirant de l'addition d'entiers
binaires.
extract_min_tree : 'a heap -> 'a tree * 'a
heap
qui isole un arbre dont la tête est de priorité minimale dans
une file, et renvoie cet arbre, ainsi que la file privée de cet
arbre.extract_min : 'a heap -> 'a * 'a
heap
qui extrait d'une file une valeur de priorité minimale de la
file. Vérifier que cette fonction a une complexité logarithmique en le
nombre d'éléments de la file.let () = let h = empty in let h = insert h 1 "a" in let h = insert h 1 "b" in assert (fst (extract_min h) = "a"); let h = insert h 1 "c" in assert (fst (extract_min h) = "a")
Déposer le fichier BinomialHeap.ml :
Nous allons finalement utiliser le module des files binomiales pour créer
un module PriorityQueue
implémentant les files de priorité
(de type 'a heap
).
create : unit -> 'a heap
permettant de
créer une file de priorité.push : 'a heap -> int -> 'a -> unit
qui permet d'ajouter un élément, avec une priorité donnée, à une file.pop : 'a heap -> 'a
permettant
d'extraire l'élément de priorité minimale le plus ancien dans la file.let () = let q = create () in push q 2 "a"; push q 1 "b"; assert (pop q = "b"); assert (pop q = "a")et
let () = let q = create () in push q 1 "a"; push q 2 "b"; assert (pop q = "a"); assert (pop q = "b")
is_empty : 'a heap -> bool
permettant de déterminer si une file est vide.size : 'a heap -> int
déterminant le
nombre d'éléments dans une file. Comment l'implémenter avec une complexité
logarithmique en le nombre de ces éléments ?Déposer le fichier PriorityQueue.ml :
Le module des files de priorité peut être utilisé pour implémenter l'algorithme de Dijkstra qui permet de trouver le plus court chemin dans un graphe dirigé pondéré (chaque arrête est étiquetée par un entier indiquant sa longueur). On trouvera les fonctions de test mentionnées ci-dessous dans le fichier Graph.ml.
pop : 'a heap -> int
* 'a
pour qu'elle revoie la priorité en plus de la valeur.
let graph edges = let vertices = List.fold_left (fun v (s,d,t) -> max v (max s t)) 0 edges + 1 in let g = Array.init vertices (fun _ -> Array.make vertices 0) in List.iter (fun (s,d,t) -> g.(s).(t) <- d) edges; g
let dijkstra g source = let pq = create () in let vertices = Array.length g in let d = Array.make vertices max_int in d.(source) <- 0; push pq 0 source; while not (is_empty pq) do let pi, i = pop pq in for j = 0 to vertices - 1 do if g.(i).(j) > 0 then let dj = d.(i) + g.(i).(j) in if dj < d.(j) then begin d.(j) <- dj; push pq dj j end done done; d
let () = let g = graph [ (1,2,2); (1,4,3); (2,1,3); (2,4,4); (2,2,5); (3,3,5); (4,2,6); (5,3,4); (5,2,6); ] in let d = dijkstra g 1 in for i = 0 to Array.length g - 1 do Printf.printf "distance to %d : %d\n" i d.(i) donedoit afficher le résultat suivant :
distance to 0 : 4611686018427387903 distance to 1 : 0 distance to 2 : 2 distance to 3 : 3 distance to 4 : 6 distance to 5 : 4 distance to 6 : 6
Une solution vous est proposée.