ANNEXE - IMPLEMENTATION
 

Cet annexe contient des détails sur les implémentations des méthodes présentées dans le mémoire que nous n'avons pas jugés importants de faire apparaître directement dans notre étude. L'objectif de cet annexe est de fournir un supplément d'informations qui peuvent aider dans l'interprétation des résultats, dans leur reproduction et dans l'intégration des divers algorithmes dans un logiciel de conception et/ou de présentation de documents hypermédia synchronisés.

Dans une première partie, nous présentons des algorithmes qui viennent compléter ceux détaillés dans les chapitres précédents. Ensuite, nous expliquons les méthodes que nous avons utilisées pour générer nos instances de problèmes de flot et de tension. Nous présentons également très brièvement les structures de données employées pour représenter les différents éléments manipulés dans nos algorithmes: graphe, cycle, cocycle... Nous terminons enfin par quelques paragraphes sur la manière dont ont été menés les tests: la machine, le système d'exploitation, le compilateur, le nombre d'instances...

 
9.1. ALGORITHMES
 
 
9.1.1. Arbre recouvrant
 

L'algorithme présenté ici construit un arbre recouvrant d'un graphe . L'arbre est obtenu sous la forme d'une fonction qui associe à un noeud l'arc qui le connecte au reste de l'arbre. Pour établir cette fonction, l'algorithme parcours le graphe, en partant d'un noeud quelconque et en employant les arcs indifféremment dans le sens direct ou indirect, et si est le premier arc qui a permis de visiter . Cette méthode étant un simple parcours de graphe, elle nécessite opérations.

pour tout faire   ;
soit un noeud de ;
  ;

tant que faire
 soit un noeud de ;
   ;

 pour tout faire
  si et alors     ;
 fin pour;

 pour tout faire
  si et alors     ;
 fin pour;
fin tant que;
 
9.1.2. Base de cycles
 

Cet algorithme permet de construire une base de cycles pour un graphe . Sa justification et son fonctionnement sont présentés à la section 2.2.1. La méthode repose sur la manipulation d'un arbre recouvrant . En lui ajoutant un par un les arcs de , cela introduit à chaque fois un cycle qui est alors inséré à la base . Cette méthode nécessite opérations, il y a arcs à ajouter et chaque fois une détection du cycle ainsi créé est nécessaire, ce qui s'effectue au pire en opérations (puisqu'il y a arcs dans l'arbre).

trouver un arbre recouvrant défini par la fonction ;
  ;
  ;

tant que faire
 pour tout faire   faux;
 /* Marque pour trouver le cycle. */

 soit un arc de ;
   ;
   ;
   ;

 tant que faire
  soit tel que ou ;
    vrai;
    ;
 fin tant que;

 tant que non faire
  soit tel que ou ;
    ;
 fin tant;

 soit le cycle formé des chaînes de à ,
 de à et de l'arc ;

   ;
fin tant que;
 
9.1.3. Plus court chemin
 

Nous rappelons ici l'algorithme dont l'idée générale a été proposée par L.R. Ford en 1956 pour la recherche d'un plus court chemin entre un noeud et tous les autres noeuds dans un graphe . Les longueurs des arcs peuvent être négatives et des circuits peuvent exister dans le graphe. La procédure consiste à repérer un arc qui ne satisfait pas les conditions d'optimalité du plus court chemin (cf. section 3.2.4) et à modifier le potentiel de l'une de ses extrémités pour vérifier la condition, cette opération est répétée jusqu'à ne plus détecter aucun arc. Cette idée a été reprise par R.E. Bellman en 1958 (cf. [Bellman58]) en proposant un ordre pour traiter les arcs qui réduit la complexité de la méthode à opérations. Elle est toujours la meilleure complexité fortement polynômiale à ce jour. Cette méthode permet également de détecter la présence d'un circuit de longueur négative (dont la somme des longueurs des arcs est négative): lorsqu'un noeud possède un potentiel inférieur à ( étant la longueur maximale d'un arc). Dans l'algorithme que nous proposons, les plus courts chemins sont obtenus sous la forme d'une fonction qui associe à un noeud l'arc par lequel le plus court chemin de arrive.

pour tout faire
   /* Le potentiel du noeud. */
   ;
fin pour;

  ;
  ;
  ;

tant que ou faire
 si alors
  sortir un noeud de /* Gestion FIFO de l'ensemble. */
 sinon sortir un noeud de /* Gestion FIFO de l'ensemble. */

 pour tout faire
  si alors
   si alors   
   sinon   

     ;
   si alors /* Circuit de longueur négative. */
     ;
  fin si;
 fin pour;
fin tant que;
 
9.1.4. Tri topologique
 

Considérons un graphe sans cycle, avec un seul noeud source. Le tri topologique des noeuds d'un tel graphe consiste à les ordonner de sorte qu'un noeud est toujours après tous ses prédécesseurs. L'algorithme proposé ici est un simple parcours des noeuds du graphe, en partant de sa source . Les noeuds sont numérotés dans l'ordre dans lequel ils sont parcourus, un noeud n'étant visité qu'après tous ses prédécesseurs, ce qui correspond bien à un ordre topologique. Cette méthode n'étant qu'un simple parcours, elle nécessite opérations.

pour tout faire
   /* Marque pour le parcours topologique. */
   /* Numéro dans l'ordre topologique. */
fin pour;

  ;
  ;

tant que faire
 choisir dans ;
   ;

 pour tout faire
    ;

  si alors
     ;
     ;
     ;
  fin pour;
 fin si;
fin tant que;
 
9.2. GENERATION ALEATOIRE DE PROBLEMES
 

La génération de problèmes pour nos jeux d'essais a posé deux difficultés, la première concerne la structure même des graphes, il a fallu créer des graphes connexes (la non connexité ne pose aucune difficulté pour les problèmes que l'on traite, mais nécessite de détecter les composantes connexes et de lancer les méthodes de résolution sur chacune). Il peut être intéressant aussi de générer des graphes sans circuit. Nous avons également généré des graphes série-parallèles et presque série-parallèles. La seconde difficulté consiste à créer les données mêmes du problème, c'est-à-dire les capacités de flot ou de tension selon le problème.

 
9.2.1. Graphes connexes
 

Pour générer des graphes connexes avec noeuds et arcs, nous avons choisi de créer aléatoirement tout d'abord un arbre connectant les noeuds du graphe. Ensuite, arcs sont ajoutés au hasard pour compléter le graphe. Lorsqu'un arc est inséré dans la deuxième phase, il faut éventuellement vérifier qu'il n'introduit pas de circuit, si c'est le cas il faut alors générer au hasard un autre arc. Pour créer l'arbre, la procédure consiste à ajouter l'un après l'autre les noeuds dans le graphe, en les connectant à chaque fois par un arc avec un noeud déjà présent dans le graphe choisi au hasard.

 
9.2.2. Graphes série-parallèles ou presque
 

La génération de graphes série-parallèles est simple. Elle repose sur la définition même de cette classe de graphes (cf. section 5.1.1). Le graphe se réduit au départ à un seul arc, ensuite à chaque itération, un arc du graphe est sélectionné au hasard pour être dédoublé, soit par une opération série, soit par une opération parallèle. Mais chaque opération série ajoute un nouveau noeud et un nouvel arc, alors que chaque opération parallèle ajoute seulement un arc. Cela signifie qu'il faut s'assurer qu'il y a exactement opérations séries d'ajoutées dans le graphe et opérations parallèles. Il y a plusieurs possibilités pour contrôler ces quotas. Nous avons choisi d'appliquer au hasard, mais uniformément, soit une opération série, soit une opération parallèle quand cela est possible. Une opération série n'est ajoutée que si le nombre de noeuds limite n'a pas été atteint, et une opération parallèle n'est appliquée que si le nombre de noeuds restants à ajouter est inférieur strictement au nombre d'arcs restants.

Générer un graphe presque série-parallèle est très simple. A partir d'un SP-graphe, il suffit d'ajouter au hasard autant d'arcs que nécessaire pour compléter le graphe. Nous verrons pour la génération des problèmes de tension qu'il peut être possible de choisir d'inverser le sens d'un arc au moment où il est inséré, pour se rapprocher des problèmes de synchronisation hypermédia qui ne manipulent que des tensions positives.

 
9.2.3. Problèmes de flot
 

Nous proposons ici une méthode pour générer des problèmes dits de flot compatible qui peuvent être étendus très simplement à des problèmes de flot maximal ou de flot de coût minimal.

Soit et deux fonctions qui associent une valeur (réelle ou entière) à chaque arc du graphe telles que . et sont les valeurs minimales et maximales du flot qui peut traverser l'arc . Le problème du flot compatible est de trouver un flot sur tel que .

On s'intéresse ici à générer un graphe avec des valeurs de flot minimales et maximales sur les arcs telles qu'il existe au moins un flot compatible. Pour cela, on s'appuie sur la preuve de la section 2.2.1.3 qui permet d'affirmer le corollaire suivant. Tout flot sur un graphe est déterminé par ses seules composantes sur les arcs qui ne sont pas sur un arbre recouvrant donné.

Autrement dit, si on veut générer aléatoirement un flot sur un graphe , il suffit de trouver un arbre recouvrant de et de tirer totalement au hasard des valeurs de flot pour les arcs qui ne font pas partie de l'arbre. Ensuite, il faut déterminer les valeurs de flot pour les arcs de l'arbre afin d'obtenir la conservation des flots. Pour cela nous proposons la méthode suivante.

On choisit un noeud de l'arbre qui n'a pas de successeur. On cherche à déterminer le flot de l'arc qui le connecte au reste de l'arbre. Pour cela on calcule la valeur suivante.

Pour garantir la conservation des flots sur le noeud, il suffit de poser . Ensuite on retire le noeud et l'arc de l'arbre et on recommence avec un autre noeud. On constate à chaque itération que l'on fixe le flot d'un arc. On obtient finalement bien un flot. Il suffit ensuite de tirer au hasard un intervalle pour chaque arc autour du flot généré et on obtient un graphe qui admet au moins un flot compatible.

 
9.2.4. Problèmes de tension
 

De manière similaire au flot, nous proposons une méthode pour générer des problèmes de tension compatible qui peuvent être étendus très simplement à des problèmes de tension maximale ou de tension de coût minimal.

Soit et deux fonctions qui associent une valeur (réelle ou entière) à chaque arc du graphe telles que . et sont les valeurs minimales et maximales de la tension de l'arc . Nous rappelons que le problème de la tension compatible est de trouver une tension sur telle que .

On s'intéresse ici à générer un graphe avec des valeurs de tension minimales et maximales sur les arcs telles qu'il existe au moins une tension compatible. Pour cela, il suffit de tirer au hasard un potentiel pour chaque noeud du graphe. Ensuite, il faut calculer la tension des arcs du graphe à l'aide de la définition 2.5. On obtient bien ainsi une tension. Il suffit alors de tirer au hasard un intervalle pour chaque arc autour de la tension générée et on obtient un graphe qui admet au moins une tension compatible.

Il faut noter que jusqu'à présent, aucune contrainte n'a été imposée au signe de la tension. Avec la synchronisation hypermédia, la tension de tout arc doit être positive. La méthode de génération présentée ici ne garantit pas pour les instances qu'il existe une tension compatible positive. Pour cette raison, la méthode de génération aléatoire est légèrement modifiée pour les problèmes de synchronisation hypermédia. Au moment du calcul de la tension d'un arc, si celle-ci s'avère négative, alors l'arc est inversé, i.e. la source devient la destination et vice-versa.

Pour les graphes presque série-parallèles, nous avons opté pour une approche légèrement différente. En effet, une fois le graphe série-parallèle généré, il est possible de le parcourir à partir de la source et d'attribuer un potentiel de plus en plus important à mesure de l'avancée dans le parcours (qui doit être bien sûr topologique). Dans une deuxième phase, des arcs perturbateurs sont ajoutés pour rendre le graphe presque série-parallèle. Pour cela, deux noeuds sont choisis au hasard et un arc est créé entre les deux, si la tension de cet arc est négative, il faut inverser ses extrémités.

 
9.3. STRUCTURES DE DONNEES
 

Une seule structure de données a été utilisée pour représenter les graphes dans les différents algorithmes implémentés. La modélisation objet est résumée dans la section 8.1. L'approche choisie est une liste d'adjacence. Un graphe est donc composé d'une liste d'arcs et d'une liste de noeuds. Chaque noeud possède une liste de ses arcs entrants et une liste de ses arcs sortants. Pour la plupart des algorithmes, la structure même de la liste a peu d'importance, car les procédures correspondent principalement à des parcours de ces listes.

Opération Complexité
Ajout d'un noeud
Ajout d'un arc
Suppression d'un noeud
Suppression d'un arc
Accès à un noeud (par sa clé)
Accès à un arc (par sa clé)
Accès à l'origine d'un arc
Accès à la destination d'un arc
Parcours des arcs entrants d'un noeud
Parcours des arcs sortants d'un noeud
Parcours des noeuds du graphe
Parcours des arcs du graphe
 
Tableau 9.1: Complexité des opérations sur la structure de graphe.

Cependant, pour la méthode d'agrégation notamment, il est nécessaire de retirer des noeuds et des arcs du graphe. A l'inverse, la méthode de reconstruction par exemple ajoute des noeuds et des arcs. Il est indispensable que ces opérations s'effectuent de manière efficace. Malheureusement, il est impossible de garantir une complexité pour toutes ces opérations à la fois, nous avons donc choisi une structure qui propose la même complexité pour toutes les opérations. Les listes sont modélisées sous la forme d'arbres binaires de recherche (la classe map en C++), la clé étant l'identifiant des éléments stockés. Toutes les opérations dans une map sont en opérations ( étant le nombre d'éléments de la structure). Seul le parcours de tous les éléments nécessite opérations. Le tableau 9.1 résume la complexité de chaque opération, il faut juste noter que l'accès aux éléments par leur clé est à éviter, il est possible d'obtenir un accès en par leur référence. L'accès par clé est normalement très rare.

Très souvent, les algorithmes nécessitent de manipuler des ensembles de noeuds ou d'arcs. Nous avons choisi comme structure de données le vecteur (la classe vector en C++). Elle garantit un accès à un élément en opérations, un parcours en ( étant le nombre d'éléments), et un ajout et une suppression en fin de liste en opérations. Quand il est nécessaire de gérer une politique (First In, First Out), une file d'attente est utilisée (la classe deque en C++) qui garantit les mêmes complexités que le vecteur avec en plus un ajout et une suppression en tête de liste en opérations.

Il faut noter que ces structures, vector et deque, reposent sur la structure de tableau, elles réallouent automatiquement de la mémoire si nécessaire, ce qui peut conduire dans certaines utilisations à une perte de performance d'environ . Lorsque les ensembles manipulés sont triés, nous avons choisi d'utiliser la structure de map, qui permet un accès en au premier élément, toutes les autres opérations étant en ( étant le nombre d'éléments).

 
9.4. RESULTATS NUMERIQUES
 

Voici quelques détails supplémentaires sur les expérimentations menées. Les programmes ont été écrits en C++ et compilés avec GNU Compiler Collection (GCC) 2.95.3, sous le système d'exploitation Unix AIX 4, avec une machine RISC-6000 à 160 MHz. L'option de compilation -O3 a été utilisée pour optimiser la vitesse des programmes.

Les expérimentations ont été menées sur des graphes générés aléatoirement avec les méthodes présentées précédemment. Pour prélever les résultats numériques, instances de chaque problème avec les mêmes caractéristiques ont été générées. Les résultats présentés dans le mémoire sont donc des moyennes des données ainsi obtenues, ce qui atténue l'influence d'une instance particulière sur les résultats.

 
 
Copyright (c) 1999-2016 - Bruno Bachelet - bruno@nawouak.net - http://www.nawouak.net
La permission est accordée de copier, distribuer et/ou modifier ce document sous les termes de la licence GNU Free Documentation License, Version 1.1 ou toute version ultérieure publiée par la fondation Free Software Foundation. Voir cette licence pour plus de détails (http://www.gnu.org).