Précédent Index Suivant

Recherche de chemins de moindre coût

La recherche de chemin de moindre coût dans un graphe orienté est utilisée dans de nombreuses applications. On s'intéresse ici à la détermination d'un itinéraire de voyage. Pour cela on représente les différentes étapes et routes par un graphe orienté, valué par le coût entre deux étapes. On utilise l'algorithme de Dijkstra pour la détermination du chemin de moindre coût entre deux étapes. Cette application va être l'occasion d'utiliser les bibliothèques présentées précédemment. On peut citer dans l'ordre d'apparition, les modules Genlex et Printf pour les entrées-sorties, le module Weak pour implanter un cache, le module Sys pour compter le temps gagné par le cache et la bibliothèque Upi pour construire l'interface graphique. On réutilise le module Sys à la construction d'une application autonome en récupérant le nom du fichier de description du graphe comme argument de la ligne de commande.

Représentation des graphes

Un graphe orienté valué est défini par son ensemble de sommets, son ensemble d'arcs et l'ensemble des valeurs associées à chaque arc. Il existe de nombreuses implantations du type de données graphe orienté valué. La pertinence de ces représentations varie selon la taille du graphe et le nombre d'arcs. Comme le propos de cette application est en partie de montrer comment mémoriser certains calculs déjà effectués sans remplir la mémoire, on utilise une matrice d'adjacence pour les graphes orientés valués. Ainsi l'occupation mémoire ne sera pas perturbée par la manipulation de listes.

# type cout = Nan | Cout of float;;
# type mat_adj = cout array array;;
# type 'a graphe = { mutable ind : int;
taille : int;
sommets : 'a array;
m : mat_adj};;
Le champ taille contient le nombre maximal de sommets et le champs ind, le nombre courant de sommets.

On définit ensuite quelques fonctions permettant de créer pas à pas un arc.

La fonction de création d'un graphe prend en arguments un sommet et le nombre maximal de sommets.

# let cree_graphe s t =
{ ind = 0; taille = t; sommets = Array.create t s;
m = Array.create_matrix t t Nan } ;;
val cree_graphe : 'a -> int -> 'a graphe = <fun>


La fonction appartient vérifie si un sommet n est dans le graphe g.

# let appartient s g =
let rec aux i =
(i < g.taille) & ((g.sommets.(i) = s) or (aux (i+1)))
in aux 0;;
val appartient : 'a -> 'a graphe -> bool = <fun>


La fonction index retourne l'indice d'un sommet s dans un graphe g. Si ce sommet n'existe pas alors l'exception Not_found est déclenchée.

# let index s g =
let rec aux i =
if i >= g.taille then raise Not_found
else if g.sommets.(i) = s then i
else aux (i+1)
in aux 0 ;;
val index : 'a -> 'a graphe -> int = <fun>


Les deux fonctions suivantes ajoutent respectivement un sommet et un arc entre deux sommets de coût c à un graphe.

# let ajoute_sommet s g =
if g.ind = g.taille then failwith "le graphe est plein"
else if appartient s g then failwith "le sommet existe déjà"
else (g.sommets.(g.ind) <- s; g.ind <- g.ind + 1) ;;
val ajoute_sommet : 'a -> 'a graphe -> unit = <fun>
# let ajoute_arc s1 s2 c g =
try
let x = index s1 g and y = index s2 g in
g.m.(x).(y) <- Cout c
with Not_found -> failwith "sommet inexistant" ;;
val ajoute_arc : 'a -> 'a -> float -> 'a graphe -> unit = <fun>


On peut alors facilement créer un graphe orienté valué complet à partir d'une liste de sommets et d'arcs. La fonction test_aho construit le graphe de la figure 13.8 :

# let test_aho () =
let g = cree_graphe "rien" 5 in
List.iter (fun x -> ajoute_sommet x g) ["A"; "B"; "C"; "D"; "E"];
List.iter (fun (a,b,c) -> ajoute_arc a b c g)
["A","B",10.;
"A","D",30.;
"A","E",100.0;
"B","C",50.;
"C","E",10.;
"D","C",20.;
"D","E",60.];
for i=0 to g.ind -1 do g.m.(i).(i) <- Cout 0.0 done;
g;;
val test_aho : unit -> string graphe = <fun>
# let a = test_aho();;
val a : string graphe =
{ind=5; taille=5; sommets=[|"A"; "B"; "C"; "D"; "E"|];
m=[|[|Cout 0; Cout 10; Nan; Cout 30; Cout ...|]; ...|]}




Figure 13.8 : Graphe de test


Construction de graphes

Il peut être assez fastidieux de devoir construire les graphes directement dans un programme. Pour éviter cela, nous définissons un format pour représenter les graphes sous une forme textuelle. On peut alors lire, à partir d'un fichier texte, les informations d'un graphe et le construire.

Un fichier de description d'un graphe contient une suite de lignes. Chaque ligne suit un des formats suivants : Par exemple, le fichier aho.dat suivant décrit le graphe de la figure 13.8 :
TAILLE 5
SOMMET A
SOMMET B
SOMMET C
SOMMET D
SOMMET E
ARC A B 10.0
ARC A D 30.0
ARC A E 100.0
ARC B C 50.
ARC C E 10.
ARC D C 20.
ARC D E 60.
Pour analyser de tels fichiers, on utilise le module Genlex d'analyse lexicale. L'analyseur lexicale est construit à partir de la liste de mots clés keywords.

La fonction parser_line effectue les actions associées aux mots clés en modifiant une référence sur un graphe.

# let keywords = [ "TAILLE"; "SOMMET"; "ARC"; "#"];;
val keywords : string list = ["TAILLE"; "SOMMET"; "ARC"; "#"]
# let lexer_line l = Genlex.make_lexer keywords (Stream.of_string l);;
val lexer_line : string -> Genlex.token Stream.t = <fun>
# let parser_line g s = match s with parser
[< '(Genlex.Kwd "TAILLE"); '(Genlex.Int n) >] ->
g := cree_graphe "" n
| [< '(Genlex.Kwd "SOMMET"); '(Genlex.Ident nom) >] ->
ajoute_sommet nom !g
| [< '(Genlex.Kwd "ARC"); '(Genlex.Ident n1);
'(Genlex.Ident n2); '(Genlex.Float c) >] ->
ajoute_arc n1 n2 c !g
| [< '(Genlex.Kwd "#") >] -> ()
| [<>] -> () ;;
val parser_line : string graphe ref -> Genlex.token Stream.t -> unit = <fun>


On utilise cet analyseur pour définir la fonction de création d'un graphe utilise à partir des données d'un fichier :

# let create_graphe name =
let g = ref {ind=0; taille=0; sommets =[||]; m = [||]} in
let ic = open_in name in
try
print_string ("Loading "^name^": ");
while true do
print_string ".";
let l = input_line ic in parser_line g (lexer_line l)
done;
!g
with End_of_file -> print_newline(); close_in ic; !g ;;
val create_graphe : string -> string graphe = <fun>
L'appel suivant construit un graphe à partir du fichier aho.dat.

# let b = create_graphe "PROGRAMMES/aho.dat" ;;
Loading PROGRAMMES/aho.dat: ..............
val b : string graphe =
{ind=5; taille=5; sommets=[|"A"; "B"; "C"; "D"; "E"|];
m=[|[|Nan; Cout 10; Nan; Cout 30; Cout 100|]; ...|]}


Algorithme de Dijkstra

L'algorithme de Dijkstra permet de trouver le chemin de coût minimal entre deux sommets. Le coût d'un chemin entre un sommet s1 et un sommet s2 est la somme des coûts des arcs de ce chemin. Un coût est toujours positif, c'est-à-dire que l'on ne gagne pas de repasser par un sommet. C'est une condition de correction de l'algorithme.

L'algorithme de Dijkstra calcule en fait tous les chemins de moindre coût d'une source S1 vers l'ensemble sommets atteignables depuis cette source. L'idée est de supposer connus les chemins de moindre coût de S1 vers un ensemble de sommets et d'étendre cet ensemble en considérant les sommets accessibles par un arc à partir de l'un des sommets déjà connu en retenant ceux qui augmentent le moins le coût des chemins.

Pour conserver l'état de la recherche, on définit le type etat_recherche puis une fonction de création de l'état initial :

# type etat_recherche = { chemins : int array;
deja_traites : bool array;
distances : cout array;
source :int;
nb : int};;
# let creer_etat () = { chemins = [||]; deja_traites = [||]; distances = [||];
nb = 0; source = 0};;
Le champ source contient le sommet de départ ; le champ deja_traites, les sommets dont on connaît déjà le chemin optimal à partir de la source et le champ nb, le nombre de sommets déjà traités. Le vecteur distances tient à jour les distances minimales entre la source et les autres sommets. Le vecteur chemin contient, pour chaque sommet, le sommet le précédent dans le chemin de moindre coût. On peut ainsi reconstituer un chemin de la source vers n'importe quel autre sommet.

Fonctions sur les coûts

On définit tout d'abord quatre fonctions utilitaires sur les coûts : a_cout pour tester de l'existence d'un arc, float_of_cout pour la conversion en flottant, add_cout pour l'addition de deux coûts et inf_cout vérifiant si un coût est inférieur à un autre.

# let a_cout c = match c with Nan -> false | _-> true;;
val a_cout : cout -> bool = <fun>
# let float_of_cout c = match c with
Nan -> failwith "float_of_cout"
| Cout x -> x;;
val float_of_cout : cout -> float = <fun>
# let add_cout c1 c2 = match (c1,c2) with
Cout x, Cout y -> Cout (x+.y)
| Nan, Cout y -> c2
| Cout x, Nan -> c1
| Nan, Nan -> c1;;
val add_cout : cout -> cout -> cout = <fun>
# let inf_cout c1 c2 = match (c1,c2) with
Cout x, Cout y -> x < y
| Cout x, Nan -> true
| _, _ -> false;;
val inf_cout : cout -> cout -> bool = <fun>
La valeur Nan joue un rôle particulier dans les calculs et la comparaison. Nous y reviendrons lorsque nous aurons donné la fonction principale (voir page ??).

Implantation de l'algorithme

La recherche d'un sommet servant à étendre les chemins déjà calculés est divisée en deux fonctions : premier_non_traite qui sélectionne un premier sommet non encore visité ; celui-ci sert de valeur initiale pour la fonction pp_non_traite qui retourne le sommet non encore traité dont l'ajout à l'ensemble des chemin construit est minimal.

# exception Trouve of int;;
exception Trouve of int
# let premier_non_traite er =
try
for i=0 to er.nb-1 do
if not er.deja_traites.(i) then raise (Trouve i)
done;
raise Not_found;
0
with Trouve i -> i ;;
val premier_non_traite : etat_recherche -> int = <fun>
# let pp_non_traite p er =
let si = ref p
and sd = ref er.distances.(p) in
for i=p+1 to er.nb-1 do
if not er.deja_traites.(i) then
if inf_cout er.distances.(i) !sd then
( sd := er.distances.(i);
si := i )
done;
!si,!sd;;
val pp_non_traite : int -> etat_recherche -> int * cout = <fun>


La fonction une_etape sélectionne un nouveau sommet, l'ajoute à l'ensemble des sommets traités et met à jour les chemins et distances des autres sommets non traités s'il y a lieu.

# exception No_way;;
exception No_way
# let une_etape er g =
let p = premier_non_traite er in
let np,nc = pp_non_traite p er in
if not(a_cout nc ) then raise No_way
else
begin
er.deja_traites.(np) <- true;
for i = 0 to er.nb -1 do
if not er.deja_traites.(i) then
if a_cout g.m.(np).(i) then
let ic = add_cout er.distances.(np) g.m.(np).(i) in
if inf_cout ic er.distances.(i) then (
er.chemins.(i) <- np;
er.distances.(i) <- ic
)
done;
er
end;;
val une_etape : etat_recherche -> 'a graphe -> etat_recherche = <fun>


Il ne reste plus qu'à itérer la fonction précédente pour implanter l'algorithme de Dijkstra. La fonction dij prend un sommet et un graphe puis retourne une valeur de type etat_recherche contenant les informations pour déduire le chemin de moindre coût de cette source vers n'importe quel sommet atteignable à partir de celle-ci.

# let dij s g =
if appartient s g then
begin
let i = index s g in
let er = { chemins = Array.create g.ind (-1) ;
deja_traites = Array.create g.ind false;
distances = Array.create g.ind Nan;
nb = g.ind;
source = i} in
er.deja_traites.(i) <- true;
for j=0 to g.ind-1 do
let c = g.m.(i).(j) in
er.distances.(j) <- c;
if a_cout c then er.chemins.(j) <- i
done;
try
for k = 0 to er.nb-2 do
ignore(une_etape er g)
done;
er
with No_way -> er
end
else failwith "dij: sommet inconnu";;
val dij : 'a -> 'a graphe -> etat_recherche = <fun>


La valeur Nan est donnée comme valeur initiale des distances. Elle représente une distance infinie, ce qui est cohérent avec la fonction de comparaison inf_cout. En revanche. Pour l'addition des coûts (fonction add_cout, cette valeur est considérée comme nulle. Ce qui permet une mise à jour simple du tableau des distances.

On peut dont maintenant tester la recherche selon l'algorithme de Dijkstra.

# let a = test_aho ();;
# let r = dij "A" a;;


Les résultats bruts sont les suivants :

# r.chemins;;
- : int array = [|0; 0; 3; 0; 2|]
# r.distances;;
- : cout array = [|Cout 0; Cout 10; Cout 50; Cout 30; Cout 60|]


Pour rendre ces résultats plus lisibles, nous définissons ci-dessous une fonction d'affichage.

Affichage de la solution

En utilisant le tableau chemins de la valeur calculée par la fonction dij, on n'a que le dernier arc des chemins calculés. Il faut donc, pour afficher la totalité de ces chemins, remonter récursivement jusqu'à la source.

# let aff_etat f (g,et) dest =
if appartient dest g then
let d = index dest g in
let rec aux is =
if is = et.source then Printf.printf "%a" f g.sommets.(is)
else (
let old = et.chemins.(is) in
aux old;
Printf.printf " -> (%4.1f) %a" (float_of_cout g.m.(old).(is))
f g.sommets.(is)
)
in
if not(a_cout et.distances.(d)) then Printf.printf "no way\n"
else (
aux d;
Printf.printf " = %4.1f\n" (float_of_cout et.distances.(d)));;
val aff_etat :
(out_channel -> 'a -> unit) -> 'a graphe * etat_recherche -> 'a -> unit =
<fun>
Cette fonction récursive utilise la pile d'appel pour afficher dans le bon ordre les sommets. On note l'utilisation d'un format "a" pour conserver le polymorphisme des graphes y compris pour l'affichage, d'où le paramètre fonctionnel f.

Le chemin optimal entre le sommet "A" (indice 0) et "E" (indice 4) est affiché ainsi :

# aff_etat (fun x y -> Printf.printf "%s!" y) (a,r) "E";;
A! -> (30.0) D! -> (20.0) C! -> (10.0) E! = 60.0
- : unit = ()


On indique les différentes sommets du chemin ainsi que les coûts de chaque étape.

Utilisation d'un cache

L'algorithme de Dijkstra calcule tous les chemins de moindre coût issus d'une source. On aimerait donc conserver ces valeurs en vue d'une prochaine recherche à partir de la même source. Cette mémorisation de résultats risque néanmoins d'occuper une part importante de la mémoire. D'où l'idée d'utiliser les << pointeurs faibles >>. Si on conserve les résultats des recherches à partir d'une source dans un tableau de pointeurs faibles, il devient ensuite possible, pour une nouvelle recherche, de vérifier si le calcul a déjà été effectué. Comme ce sont des pointeurs faibles, l'espace mémoire de ces états peut être libéré en cas de besoin par le GC. Cela permet de ne pas perturber le reste du programme par une occupation mémoire trop importante. Au pire des cas le calcul peut être refait s'il y a une demande.

Implantation du cache

On définit un nouveau type 'a recherche_graphe :

# type 'a recherche_graphe =
{ g : 'a graphe; w : etat_recherche Weak.t } ;;
Les champs g et w correspondent au graphe et au tableau de pointeurs faibles sur les états de recherche pour chaque source possible.

On construit de telles valeurs par la fonction create_recherche.

# let create_rech_graphe g =
{ g = g;
w = Weak.create g.ind } ;;
val create_rech_graphe : 'a graphe -> 'a recherche_graphe = <fun>


La fonction dij_rapide vérifie si la recherche a déjà été calculée, si oui, elle retourne le résultat mémorisé, sinon, elle effectue le calcul et l'enregistre dans le tableau de pointeurs faibles.

# let dij_rapide s rg =
let i = index s rg.g in
match Weak.get rg.w i with
None -> let er = dij s rg.g in
Weak.set rg.w i (Some er);
er
| Some er -> er;;
val dij_rapide : 'a -> 'a recherche_graphe -> etat_recherche = <fun>


La fonction d'affichage reste utilisable :

# let rg_a = create_rech_graphe a in
let r = dij_rapide "A" rg_a in
aff_etat (fun x y -> Printf.printf "%s!" y) (a,r) "E" ;;
A! -> (30.0) D! -> (20.0) C! -> (10.0) E! = 60.0
- : unit = ()


Évaluation des performances

Nous allons tester les performances des fonctions dij et dij_rapide en itérant chacune d'elle sur une liste de source tirée au hasard. On simule ainsi une application dans laquelle il faudrait calculer souvent un chemin entre deux points d'un graphe (un itinéraire ferroviaire, par exemple).

Pour obtenir les temps de calcul, nous définissons la fonction suivante :

# let exe_time f g ss =
let t0 = Sys.time() in
Printf.printf "Début (%5.2f)\n" t0;
List.iter (fun s -> ignore(f s g)) ss;
let t1 = Sys.time() in
Printf.printf "Fin (%5.2f)\n" t1;
Printf.printf "Durée = (%5.2f)\n" (t1 -. t0) ;;
val exe_time : ('a -> 'b -> 'c) -> 'b -> 'a list -> unit = <fun>


On tire donc au hasard une liste de 20000 sommets et on mesure les performances contenues sur le graphe a :

# let ss =
let ss0 = ref [] in
let i0 = int_of_char 'A' in
let new_s i = Char.escaped (char_of_int (i0+i)) in
for i=0 to 20000 do ss0 := (new_s (Random.int a.taille))::!ss0 done;
!ss0 ;;
val ss : string list =
["A"; "B"; "D"; "A"; "E"; "C"; "B"; "B"; "D"; "E"; "B"; "E"; "C"; "E"; "E";
"D"; "D"; "A"; "E"; ...]
# Printf.printf"Fonction dij :\n";
exe_time dij a ss ;;
Fonction dij :
Début ( 7.08)
Fin ( 8.02)
Durée = ( 0.94)
- : unit = ()
# Printf.printf"Fonction dij_rapide :\n";
exe_time dij_rapide (create_rech_graphe a) ss ;;
Fonction dij_rapide :
Début ( 8.02)
Fin ( 8.13)
Durée = ( 0.11)
- : unit = ()


Les résultats obtenus sont cohérents. Il est clair que l'accès direct au résultat contenu dans le cache est bien plus rapide que la reconstruction du résultat.

Interface graphique

On se propose de construire l'interface graphique de visualisation des graphes en utilisant la bibliothèque Upi. Cette interface permet de sélectionner les sommets origine et destination du chemin à rechercher. Une fois le chemin de moindre coût trouvé, celui-ci s'affiche graphiquement. On définit le type 'a gg qui regroupe les champs de description de graphe et de la recherche, ainsi que les champs de l'interface graphique.

# #load "PROGRAMMES/upi.cmo";;

# type 'a gg = { mutable src : 'a * Upi.component;
mutable dest : 'a * Upi.component;
pos : (int * int) array;
rg : 'a recherche_graphe;
mutable etat : etat_recherche;
mutable main : Upi.component;
to_string : 'a -> string;
from_string : string -> 'a } ;;


Les champs src et dest sont des couples (sommet,composant) liant un sommet à un composant. Le champ pos contient les positions de chaque composant. Le champ main est le conteneur principal de l'ensemble des composants. Les deux fonctions to_string et from_string sont les fonctions de conversion du type 'a avec les chaînes. Les éléments nécessaires pour la construction de telles valeurs sont les informations du graphe, le tableau de positions et les fonctions de conversion.

# let cree_gg rg vpos ts fs = 
{src = rg.g.sommets.(0),Upi.empty_component;
dest = rg.g.sommets.(0),Upi.empty_component;
pos = vpos;
rg = rg;
etat = creer_etat () ;
main = Upi.empty_component;
to_string = ts;
from_string = fs};;
val cree_gg :
'a recherche_graphe ->
(int * int) array -> ('a -> string) -> (string -> 'a) -> 'a gg = <fun>




Visualisation

Le dessin du graphe nécessite de dessiner les sommets et de tracer les arcs. Les sommets sont représentés par des composants boutons de la bibliothèque Upi. Par contre les arcs sont directement tracés dans la fenêtre principale. La fonction display_arc affiche les différents arcs. La fonction display_shortest_path affiche dans une autre couleur le chemin trouvé.

Tracé des arcs
Un arc relie deux sommets et possède une valeur associée. Le lien entre deux sommets peut être représenté par un tracé de ligne. La difficulté principale provient de l'affichage du sens de cette ligne. On choisit de le représenter par une flèche. Pour être bien orientée, celle-ci doit subir une rotation de l'angle que fait la ligne avec l'axe des abscisses. Enfin les coûts doivent être affichés à côté de l'arc.

Pour le tracé d'une flèche d'un arc, on définit : les fonctions rotate et translate qui effectuent respectivement une rotation et une translation ; la fonction display_arrow dessine la flèche dont l'extrémité est en fait un triangle.

# let rotate l a =
let ca = cos a and sa = sin a in
List.map (function (x,y) -> ( x*.ca +. -.y*.sa, x*.sa +. y*.ca)) l;;
val rotate : (float * float) list -> float -> (float * float) list = <fun>
# let translate l (tx,ty) =
List.map (function (x,y) -> (x +. tx, y +. ty)) l;;
val translate :
(float * float) list -> float * float -> (float * float) list = <fun>
# let display_arrow (mx,my) a =
let triangle = [(5.,0.); (-3.,3.); (1.,0.); (-3.,-3.); (5.,0.)] in
let tr = rotate triangle a in
let ttr = translate tr (mx,my) in
let tt = List.map (function (x,y) -> (int_of_float x, int_of_float y)) ttr
in
Graphics.fill_poly (Array.of_list tt);;
val display_arrow : float * float -> float -> unit = <fun>


L'affichage du coût d'un arc positionne le point où démarre le texte en fonction de l'angle de l'arc.

# let display_label (mx,my) a lab =
let (sx,sy) = Graphics.text_size lab in
let pos = [ float(-sx/2),float(-sy) ] in
let pr = rotate pos a in
let pt = translate pr (mx,my) in
let px,py = List.hd pt in
let ox,oy = Graphics.current_point () in
Graphics.moveto ((int_of_float mx)-sx-6)
((int_of_float my) );
Graphics.draw_string lab;
Graphics.moveto ox oy;;
val display_label : float * float -> float -> string -> unit = <fun>


L'affichage d'un arc reprend les fonctions précédentes. Ses paramètres sont le graphe interfacé gg, les sommets i et j, et la couleur du tracé (col).

# let display_arc gg col i j =
let g = gg.rg.g in
let x,y = gg.main.Upi.x,gg.main.Upi.y in
if a_cout g.m.(i).(j) then (
let (a1,b1) = gg.pos.(i)
and (a2,b2) = gg.pos.(j) in
let x0,y0 = x+a1,y+b1 and x1,y1 = x+a2,y+b2 in
let rxm = (float(x1-x0)) /. 2. and rym = (float(y1-y0)) /. 2. in
let xm = (float x0) +. rxm and ym = (float y0) +. rym in
Graphics.set_color col;
Graphics.moveto x0 y0;
Graphics.lineto x1 y1;
let a = atan2 rym rxm in
display_arrow (xm,ym) a;
display_label (xm,ym) a
(string_of_float(float_of_cout g.m.(i).(j))));;
val display_arc : 'a gg -> Graphics.color -> int -> int -> unit = <fun>


Affichage d'un chemin
L'affichage d'un chemin applique le tracé des différents arcs par lesquels passe le chemin. Cet affichage du chemin vers une destination reprend la technique utilisée pour l'affichage texte.

# let rec display_shortest_path gg col dest =
let g = gg.rg.g in
if appartient dest g then
let d = index dest g in
let rec aux is =
if is = gg.etat.source then ()
else (
let old = gg.etat.chemins.(is) in
display_arc gg col old is;
aux old )
in
if not(a_cout gg.etat.distances.(d)) then Printf.printf "no way\n"
else aux d;;
val display_shortest_path : 'a gg -> Graphics.color -> 'a -> unit = <fun>


Affichage du graphe
La fonction display_gg affiche un graphe complet. Dans le cas où le sommet destination est non vide, elle trace le chemin entre l'origine et la destination.

# let display_gg gg () =
Upi.display_rect gg.main ();
for i=0 to gg.rg.g.ind -1 do
for j=0 to gg.rg.g.ind -1 do
if i<> j then display_arc gg (Graphics.black) i j
done
done;
if snd gg.dest != Upi.empty_component then
display_shortest_path gg Graphics.red (fst gg.dest);;
val display_gg : 'a gg -> unit -> unit = <fun>


Il reste à dessiner les sommets. Comme l'interaction avec l'utilisateur proviendra de la sélection des sommets source et destination, nous définissons un composant pour les sommets.

Composant sommet

La principale action d'un utilisateur est de choisir les extrémités du chemin à rechercher. Pour cela un sommet doit être un composant qui réagit au clic souris et posséder un état lui indiquant si l'on vient de choisir la source ou la destination. On choisit le composant bouton qui effectue une action à la suite d'un clic souris.

Action des sommets
Il est nécessaire de visualiser la sélection d'un sommet. Pour cela on inverse la couleur du fond de celui-ci par la fonction inverse.

# let inverse b =
let gc = Upi.get_gc b in
let fcol = Upi.get_gc_fcol gc
and bcol = Upi.get_gc_bcol gc in
Upi.set_gc_bcol gc fcol;
Upi.set_gc_fcol gc bcol;;
val inverse : Upi.component -> unit = <fun>


La fonction action_b effectue cette sélection. Elle est appliquée lors d'un clic souris sur un sommet. Elle prend comme paramètres le sommet associé au bouton ainsi que le graphe pour pouvoir modifier la source ou la destination de la recherche. Quand ces deux sommets sont déterminés, alors elle applique la fonction dij_rapide pour trouver le chemin de moindre coût.
            
# let action_clic som gg b bs =
let (s1,s) = gg.src
and (s2,d) = gg.dest in
if s == Upi.empty_component then (
gg.src <- (som,b); inverse b )
else
if d == Upi.empty_component then (
inverse b;
gg.dest <- (som,b);
gg.etat <- dij_rapide s1 gg.rg;
display_shortest_path gg (Graphics.red) som
)
else (inverse s; inverse d;
gg.dest <- (s2,Upi.empty_component);
gg.src <- som,b; inverse b);;
val action_clic : 'a -> 'a gg -> Upi.component -> 'b -> unit = <fun>


Création de l'interface
La fonction principale de création de l'interface prend un graphe interfacé et une liste d'options et crée les différents composants puis les associe au graphe. Les paramètres sont le graphe (gg), ses dimensions (gw et gh), la liste d'options du graphes et des sommets (lopt) et la liste d'options pour les bords des sommets (lopt2).
  
# let maj_gg gg gw gh lopt lopt2 =
let gc = Upi.make_default_context () in
Upi.set_gc gc lopt;
(* calcul de la taille max des boutons *)
let vs = Array.map gg.to_string gg.rg.g.sommets in
let vsize = Array.map Graphics.text_size vs in
let w = Array.fold_right (fun (x,y) -> max x) vsize 0
and h = Array.fold_right (fun (x,y) -> max y) vsize 0 in
(* création de main *)
gg.main <- Upi.create_panel true gw gh lopt;
gg.main.Upi.display <- display_gg gg;
(* et création des boutons *)
let vb_bs =
Array.map (fun x -> x,Upi.create_button (" "^(gg.to_string x)^" ")
lopt)
gg.rg.g.sommets in
let f_act_b = Array.map (fun (x,(b,bs)) ->
let ac = action_clic x gg b
in Upi.set_bs_action bs ac) vb_bs in
let bb =
Array.map (function (_,(b,_)) -> Upi.create_border b lopt2) vb_bs
in
Array.iteri
(fun i (b) -> let x,y = gg.pos.(i) in
Upi.add_component gg.main b
["PosX",Upi.Iopt (x-w/2);
"PosY", Upi.Iopt (y-h/2)]) bb;
();;
val maj_gg :
'a gg ->
int ->
int -> (string * Upi.opt_val) list -> (string * Upi.opt_val) list -> unit =
<fun>
Les boutons sont créés automatiquement et sont bien ajoutés à la fenêtre principale.

Test de l'interface
Tout est prêt alors pour la création d'une interface. On utilise un graphe dont les sommets sont des chaînes de caractères pour simplifier les fonctions de conversion. On construit le graphe gg de la manière suivante :

# let id x = x;;
# let pos = [| 200, 300; 80, 200 ; 100, 100; 200, 100; 260, 200 |];;
# let gg = cree_gg (create_rech_graphe (test_aho())) pos id id;;
# maj_gg gg 400 400 ["Background", Upi.Copt (Graphics.rgb 130 130 130);
"Foreground",Upi.Copt Graphics.green]
[ "Relief", Upi.Sopt "Top";"Border_size", Upi.Iopt 2];;


L'appel à Upi.loop true false gg.main;; lance la boucle d'interaction de la bibliothèque Upi.



Figure 13.9 : Sélection des sommets d'une recherche


La figure 13.9 montre le chemin trouvé entre le sommet "A" et "E". Les arcs par lesquels passe le chemin changent de couleur.

Application autonome

Nous combinons les différentes étapes rencontrées pour la construction d'une application autonome, c'est-à-dire ne nécessitant pas la présence de la distribution d'Objective CAML sur la machine où elle s'exécute. Cet application prend en argument le nom du fichier de description du graphe.

Fichier de description du graphe

Ce fichier décrit d'une part les informations du graphe ainsi que les informations utiles pour l'interface graphique. Pour celles-ci nous définissons un deuxième format. De cette description graphique, on construit une valeur de type g_info suivant.

# type g_info = {spos : (int * int) array;
mutable opt : Upi.lopt;
mutable g_w : int;
mutable g_h : int};;


Le format de description des informations graphiques est décrit par les quatre mots clés de la liste key2.

# let key2 = ["HAUTEUR"; "LARGEUR"; "POSITION"; "COULEUR"];;
val key2 : string list = ["HAUTEUR"; "LARGEUR"; "POSITION"; "COULEUR"]
# let lex2 l = Genlex.make_lexer key2 (Stream.of_string l);;
val lex2 : string -> Genlex.token Stream.t = <fun>

# let pars2 g gi s = match s with parser
[< '(Genlex.Kwd "HAUTEUR"); '(Genlex.Int i) >] -> gi.g_h <- i
| [< '(Genlex.Kwd "LARGEUR"); '(Genlex.Int i) >] -> gi.g_w <- i
| [< '(Genlex.Kwd "POSITION"); '(Genlex.Ident s);
'(Genlex.Int i); '(Genlex.Int j) >] -> gi.spos.(index s g) <- (i,j)
| [< '(Genlex.Kwd "COULEUR"); '(Genlex.Ident s);
'(Genlex.Int r); '(Genlex.Int g); '(Genlex.Int b) >] ->
gi.opt <- (s, Upi.Copt (Graphics.rgb r g b))::gi.opt
| [<>] -> ();;
val pars2 : string graphe -> g_info -> Genlex.token Stream.t -> unit = <fun>


Création de l'application

La fonction create_graphe prend en entrée un nom de fichier et retourne un couple composé d'un graphe et des informations graphiques associées.

# let create_gg_graphe name =
let g = create_graphe name in
let gi = {spos = Array.create g.taille (0,0); opt=[]; g_w =0; g_h = 0;} in
let ic = open_in name in
try
print_string ("Loading (pass 2) " ^name ^" : ");
while true do
print_string ".";
let l = input_line ic in pars2 g gi (lex2 l)
done ;
g,gi
with End_of_file -> print_newline(); close_in ic; g,gi;;
val create_gg_graphe : string -> string graphe * g_info = <fun>


La fonction create_app construit l'interface d'un graphe.

# let create_app name =
let g,gi = create_gg_graphe name in
let size = (string_of_int gi.g_w) ^ "x" ^ (string_of_int gi.g_h) in
Graphics.open_graph (":0 "^size);
let gg = cree_gg (create_rech_graphe g) gi.spos id id in
maj_gg gg gi.g_w gi.g_h
[ "Background", Upi.Copt (Graphics.rgb 130 130 130) ;
"Foreground", Upi.Copt Graphics.green ]
[ "Relief", Upi.Sopt "Top" ; "Border_size", Upi.Iopt 2 ] ;
gg;;
val create_app : string -> string gg = <fun>


Enfin la fonction main récupère le nom du fichier sur la ligne de commande, construit le graphe et son interface, puis lance la boucle d'interaction sur le composant principal de l'interface du graphe.

# let main () =
if (Array.length Sys.argv ) <> 2
then Printf.printf "Usage: dij.exe filename\n"
else
let gg = create_app Sys.argv.(1) in
Upi.loop true false gg.main;;
val main : unit -> unit = <fun>


La dernière expression de ce programme lance la fonction main.

Exécutable autonome

L'intérêt de fabriquer un exécutable autonome est de faciliter sa diffusion. On regroupe l'ensemble des types et fonctions décrits dans cette section dans le fichier dij.ml. On compile ensuite ce fichier en lui joignant les différentes bibliothèques dont il se sert. Voici la ligne de compilation pour un système Linux :
ocamlc -custom -o dij.exe graphics.cma upi.cmo graphes.ml \
    -cclib -lgraphics -cclib -L/usr/X11/lib -cclib -lX11
La description de la compilation d'un exécutable autonome utilisant la bibliothèque Graphics est donnée aux chapitres 5 et 7.

Pour en faire plus

Le squelette de cette application est suffisamment généraliste pour pouvoir être réemployé dans un autre cadre que la recherche d'un itinéraire de voyage. En effet différents types de problèmes peuvent se représenter sous forme d'un graphe valué. Par exemple la recherche d'un chemin dans un labyrinthe peut se coder sous forme d'un graphe où chaque carrefour est un sommet du graphe. Trouver une solution revient à déterminer le plus court chemin entre le départ et l'arrivée.

Pour comparer les performances entre C et Objective CAML, on écrit l'algorithme de Dijkstra en C. Le programme C utilisera les structures de données Objective CAML pour effectuer le calcul.

Pour améliorer l'interface graphique, on ajoute un textfield pour le nom de fichier et deux boutons pour le chargement ou la sauvegarde d'un graphe. L'utilisateur peut aussi modifier les emplacements des sommets à la souris pour améliorer le rendu.

Une deuxième amélioration de l'interface graphique est de pouvoir choisir la forme des sommets. L'affichage d'un bouton appelle la fonction de tracé d'un rectangle. Rien n'empêche de spécialiser les fonctions d'affichage et d'appartenance par des polygones de description du sommet.






Précédent Index Suivant