Sys.command "ocaml -version";;
The OCaml toplevel, version 4.02.3
La question de programmation pour ce texte était donnée en question 1 en page 9 :
"Programmer l'algorithme PC. Faites le tourner sur l'exemple de réseau non distributif."
La consigne était très courte, mais avec aucune indication. Notez qu'il est rare que le texte exige un exemple particulier.
Ça va être assez rapide :
Si possible, on va essayer de faire des tests pour chaque fonction intermédiaire, et un exemple de plus à la fin.
On se restreint aux intervalles à coordonnées entières, et on considère des listes d'intervalles. Tous les intervalles sont fermés à gauche et à droite.
type intervalle = (int * int);; (* (a, b) représente l'intervalle [a, b] *)
type intervalles = intervalle list;;
On définit tout de suite deux exemples, $T_a$ et $S_a$ tirés de la Figure 2.a) et $T_b,S_b$ de la Figure 2.b). Cela permettra de vérifier les opérations $\oplus$ et $\otimes$.
let t_a : intervalles = [
(1, 4);
(6, 8)
];;
let s_a : intervalles = [
(0, 1);
(3, 5);
(6, 7)
];;
let t_b : intervalles = [
(-1, 0);
(2, 4)
];;
let s_b : intervalles = [
(0, 1);
(4, 4) (* Intervalle de longueur nulle *)
];;
On peut écrire des opérations d'intersection et de composition sur deux intervalles, ensuite il suffira de les généraliser à un ensemble d'intervalle.
On suit les définitions de l'énoncé.
Notez que $n \leq l + m$ ici.
Pour l'intersection de deux intervalles, l'intervalle vide $\emptyset$ peut être obtenu, donc la fonction suivante renvoie un type intervalle option
: soit None
si $I \cap J = \emptyset$, soit Some (x, y)
si $I \cap J = [x, y]$.
let intersection (i : intervalle) (j : intervalle) : intervalle option =
let a = fst i and b = snd i in
let c = fst j and d = snd j in
if b < c || d < a then
None
else
Some (max a c, min b d)
;;
Ensuite, il suffit d'explorer tous les couples $(I, J)$ possible, et de ne garder que ceux qui donnent un intervalle. On supprimera les doublons en vérifiant au fur et à mesure (ça a la même complexité que si on le fait à la fin).
En manipulant une liste d'intervalle option
, on doit ruser un peu pour n'ajouter que ceux qui ne sont pas dans acc
et qui sont des vrais intervalles.
let ajoute_nouveaux_option (acc : intervalles) (liste_option : intervalle option list) =
List.map
(fun i -> match i with Some i2 -> i2 | None -> (0, 0))
(List.filter (fun i ->
match i with
| None -> false
| Some i2 -> not (List.mem i2 acc)
) liste_option)
;;
Avec tout ça, on a une belle fonction récursive, avec un accumulateur acc
qui contient la liste des intervalles dans $T \oplus S$, construite en considérant les intervalles de $S$ les un après les autres.
On s'assure de n'avoir ni intervalles vide, ni doublon, grâce à ajoute_nouveaux_option
.
let intersections (t : intervalles) (s : intervalles) : intervalles =
let rec inter_aux acc tx sx =
match sx with
| [] -> acc (* Plus rien à ajouter *)
| j :: s2 -> (* On traite j, puis récursivement la suite de s *)
let t_inter_j = List.map (intersection j) tx in
inter_aux ((ajoute_nouveaux_option acc t_inter_j) @ acc) tx s2
in
List.sort compare (inter_aux [] t s)
(* On trie pour les avoir en ordre croissant, c'est tout *)
;;
Pour frimer un peu et simplifier l'écriture de l'algorithme PC, on peut définir une opération infixe en raccourci : $$ T \oplus S = \texttt{t ++ s}.$$
let ( ++ ) = intersections;;
Ce sera plus facile. $$ \forall T = (I_1,\dots,I_l), \forall S = (J_1,\dots,J_m),\\ T \otimes S := \{ K_1, \dots, K_n\} \;\;\text{Où}\;\; K_k = [a + c, b + d], \;\text{si}\; I_i = [a, b], J_j = [c, d]. $$ Notez que $n \leq l \times m$ ici.
Pour la composition de deux intervalles, il n'y pas de difficulté particulière :
let composition (i : intervalle) (j : intervalle) : intervalle =
let a = fst i and b = snd i in
let c = fst j and d = snd j in
(a + c, b + d)
;;
Et on les combine facilement, en gardant la même architecture que pour intersections
.
let ajoute_nouveaux (acc : intervalles) (liste : intervalles) : intervalles =
List.filter (fun i -> not (List.mem i acc)) liste
;;
let compositions (t : intervalles) (s : intervalles) : intervalles =
let rec compo_aux acc tx sx =
match sx with
| [] -> acc (* Plus rien à ajouter *)
| j :: s2 -> (* On traite j, puis récursivement la suite de s *)
let t_compo_j = List.map (composition j) tx in
compo_aux ((ajoute_nouveaux acc t_compo_j) @ acc) tx s2
in
List.sort compare (compo_aux [] t s)
(* On trie pour les avoir en ordre croissant, c'est tout *)
;;
Pour frimer un peu et simplifier l'écriture de l'algorithme PC, on peut définir une opération infixe en raccourci : $$ T \otimes S = \texttt{t ** s}.$$
let ( ** ) = compositions;;
On peut aussi rapidement définier $T \cup S$, pour l'union. C'est très facile.
let union (t : intervalles) (s : intervalles) : intervalles =
List.append t s
;;
On aimerait reproduire les exemples de la Figure 2 du texte.
t_a ++ s_a;;
On retrouve bien le résultat de la Figure 2.a).
t_a ** s_a;;
union t_a s_a;;
t_b ** s_b;;
On retrouve bien le résultat de la Figure 2.b).
L'intervalle $[3, 4]$ est inclus dans $[2, 5]$, donc on devrait ajouter une étape de nettoyage pour donner une forme canonique aux intervalles produit par composition
. On le fait plus bas.
t_b ++ s_b;;
union t_b s_b;;
On remarque que les intervalles sont bien donnés dans l'ordre croissant, puisqu'on a pensé à trier la sortie des deux fonctions, mais ça ne change rien.
On va raffiner les fonctions définis ci-dessus en ajoutant un test, sur leur résultat final.
est_inclus i j
teste si $I \subseteq J$.let est_inclus (i : intervalle) (j : intervalle) : bool =
let a = fst i and b = snd i in
let c = fst j and d = snd j in
c <= a && b <= d
;;
est_inclus (3, 4) (2, 5);; (* true *)
est_inclus (2, 5) (3, 4);; (* false *)
est_inclus (1, 1) (1, 1);; (* true *)
est_inclus_dans_un i acc
teste si $I \subseteq J$ pour un $J \neq I \in \mathrm{Acc}$.let est_inclus_dans_un (i : intervalle) (acc : intervalles) : bool =
List.exists (fun j -> (i != j) && (est_inclus i j)) acc
;;
filtre
qui retire les intervalles inclus dans d'autres, puis retire les doublons.let retire_les_inclus (liste : intervalles) : intervalles =
List.filter (fun i -> not (est_inclus_dans_un i liste)) liste
;;
let retire_les_doublons (liste : intervalles) : intervalles =
let reponse = ref [] in
List.iter (fun i ->
if not (List.mem i !reponse) then
reponse := i :: !reponse
) liste;
!reponse
;;
let filtre liste =
retire_les_doublons (retire_les_inclus liste)
;;
let intersections2 (t : intervalles) (s : intervalles) : intervalles =
List.sort compare (filtre (intersections t s))
(* On trie pour les avoir en ordre croissant, c'est tout *)
;;
let ( ++ ) = intersections2;;
t_a ++ s_a;;
let compositions2 (t : intervalles) (s : intervalles) : intervalles =
List.sort compare (filtre (compositions t s))
(* On trie pour les avoir en ordre croissant, c'est tout *)
;;
let ( ** ) = compositions2;;
t_b ** s_b;;
C'était un peu long, mais c'est propre au moins.
Notez que pour obtenir une vraie forme canonique, il faudrait aussi rassembler les intervalles consécutifs ($\{ [0, 1], [1, 2] \} \rightarrow \{ [0, 1] \}$) et se recoupant ($\{ [0, 3], [2, 4] \} \rightarrow \{ [0, 4] \}$).
Ça prendrait trop de temps. Et ce n'était pas exigé.
On a besoin désormais de considérer des réseaux STP, qui sont des graphes dont les sommets sont des entiers, et dont les arêtes sont étiquetées par des listes (non vides) d'intervalles.
L'algorithme PC demande de pouvoir accéder rapidement et facilement à l'arête entre deux sommets $x,y$, $T_{x,y}$.
Ainsi, la structure de matrice d'adjacence semble appropriée.
Les arêtes inexistantes dans le réseau auront simplement $T_{x,y} = \emptyset$, c'est-à-dire []
(liste vide).
On supposera que toutes les matrices données aux différentes fonctions définies plus bas sont carrées, on ne le vérifie pas (mais ce serait facile).
type sommet = int;;
type arete = intervalles;; (* c'est l'idée *)
type reseauSTP = intervalles array array;;
On essaie tout de suite notre structure de données avec l'exemple du réseau STP de la figure 4 :
let t_01 : intervalles = [(0, 1); (10, 20)];;
let t_12 : intervalles = [(0, 10)];;
let t_13 : intervalles = [(25, 50)];;
let t_23 : intervalles = [(0, 20); (40, 40)];;
let stp_4 : reseauSTP = [|
[| []; t_01; []; [] |];
[| []; t_12; t_13; [] |];
[| []; []; t_23; [] |];
[| []; []; []; [] |];
|];;
On peut vérifier qu'il n'est pas distributif, en prenant l'exemple du texte (fin page 8) :
let s_13 = t_12 ** t_23;;
t_13 ++ s_13;;
t_01 ** (t_13 ++ s_13);; (* 1er cas *)
t_01 ** t_13;;
t_01 ** s_13;;
(t_01 ** t_13) ++ (t_01 ** s_13);; (* 2nd cas *)
En simplifiant, on obtient :
qui sont bien différents.
Enfin, on peut rapidement vérifier si la matrice d'un graphe est bien carrée :
let est_carree matrice =
let n = Array.length matrice in
Array.fold_left (fun b x -> b && (n = (Array.length x))) true matrice
;;
est_carree stp_4;;
Il n'y a pas de boucle until
en Caml, mais avec une boucle while
on arrivera presque à la même chose.
exception Fini;; (* Pour faire le [exit]. *)
On peut l'écrire avant pour la rendre plus claire, mais l'étape clé de l'algorithme PC (et Floyd-Warshall) est une opération dite de relaxation : $$ T_{i,j} \oplus (T_{i,k} \otimes T_{k,j}).$$
let relaxe (reseau : reseauSTP) i j k =
let t_ij = reseau.(i).(j)
and t_ik = reseau.(i).(k)
and t_kj = reseau.(k).(j)
in t_ij ++ (t_ik ** t_kj)
;;
On a tout ce qu'il faut pour écrire l'algorithme.
let algorithmePC (reseau : reseauSTP) : (reseauSTP * intervalles list) =
let resT = Array.copy reseau (* on ne modifie pas l'entrée *)
and resS = ref [||] in
let n = Array.length resT
and allseen = ref [] (* Pour débogguer, je veux la liste des Tij vus *)
in
begin
try begin
while !resS != resT do
resS := Array.copy resT; (* S := T *)
for k = 0 to n - 1 do
for i = 0 to n - 1 do
for j = 0 to n - 1 do
resT.(i).(j) <-
relaxe resT i j k;
allseen := (resT.(i).(j)) :: !allseen; (* on l'ajoute *)
if resT.(i).(j) = [] then
raise Fini
done
done
done
done;
end
with Fini -> () (* On ignore l'exception, on a juste terminé. *)
end;
resT, !allseen
;;
On va traiter l'exemple de la Figure 4 du texte, comme défini plus haut :
stp_4;;
algorithmePC stp_4;;
Je ne suis pas sûr de comment interprêter ce résultat...
- soit j'ai fait une erreur dans l'implémentation,
- soit l'algorithme PC devait ne rien modifier à $T$ sur cet exemple...
On peut étudier le STP de la Figure 1., en enlevant la contrainte $[60, \infty)$, qui ne rentre pas dans notre implémentation.
.
let t_01 : intervalles = [(10, 20)];;
let t_12 : intervalles = [(30, 40)];;
let t_32 : intervalles = [(10, 20)];;
let t_34 : intervalles = [(20, 30); (40, 50)];;
let t_40 : intervalles = [(60, 70)];;
let stp_1 : reseauSTP = [|
[| []; t_01; []; []; [] |];
[| []; []; t_12; []; [] |];
[| []; []; []; []; [] |];
[| []; []; t_32; []; t_34 |];
[| t_40; []; []; []; [] |];
|];;
algorithmePC stp_1;;
Je ne suis pas sûr de comment interprêter ce résultat...
- soit j'ai fait une erreur dans l'implémentation,
- soit l'algorithme PC devait ne rien modifier à $T$ sur cet exemple...
Voilà pour la question obligatoire de programmation :
Et on a essayé de faire un peu plus, en implémentant la vérification d'une contrainte de plus.
Bien-sûr, ce petit notebook ne se prétend pas être une solution optimale, ni exhaustive.