Baniere Saphir Control
http://www.saphir-control.fr/

bar


BACK

Graphes et réseaux avec Scilab, la boîte à outils Metanet
Première partie

Claude Gomez   Maurice Goursat






Les problèmes de réseaux sont très classiques dans de nombreux domaines. Les réseaux que l'on rencontre le plus souvent sont : Pour tous ces problèmes de réseaux, les modélisations varient mais la structure sous-jacente utilisée est celle d'un graphe. Ces mêmes structures se retrouvent dans d'autres domaines, par exemple en informatique. Nous pouvons citer deux problèmes standards : celui de l'analyse et de l'optimisation d'algorithmes pour la compilation et celui de l'implantation d'algorithmes sur des réseaux de processeurs.

On dispose dans Scilab d'une boîte à outils pour définir, traiter et visualiser les graphes et les réseaux : elle s'appelle Metanet. Grâce à l'existence dans Scilab de la structure de données de listes, les graphes ont été définis en utilisant de telles structures (ici des tlist, voir Linux Magazine 14). Cela permet de manipuler facilement des graphes sous la forme de simples variables et d'avoir toute une panoplie de fonctions qui opèrent sur ces variables : les fonctions de plus bas niveau sont celles qui modifient le graphe, viennent ensuite celle qui calculent sur les graphes et enfin celles qui résolvent des problèmes de réseaux. De plus un éditeur et visualisateur graphique est disponible, ce qui est indispensable lorsque l'on veut voir ce qu'il se passe.

1   Euler se promène

Leonhard Euler (1707-1783) est considéré par certains historiens des sciences comme le plus grand mathématicien de tous les temps. C'est en particulier lui qui a découvert la célèbre relation eip=-1 où l'on retrouve trois des plus fameux symboles mathématiques.

La ville de Koenigsberg est traversée par une rivière qui possède deux branches et qui entoure une île. Il y a sept ponts pour traverser la rivière à différents endroits (voir la figure X(a)). Quand un grand mathématicien comme Euler se promène à Koenigsberg, c'est tout à fait normal qu'il se demande si un piéton, en se promenant, peut trouver un chemin lui permettant de passer sur tous les ponts en passant une fois et une seule sur chacun d'eux ? On dit que ce problème est l'un des tout premiers de la théorie des graphes.

En effet, il est possible d'associer à ce problème ce que l'on appelle un graphe non orienté dans lequel les ponts sont les arêtes du graphe et les sommets correspondent aux quatre parties de la ville délimitées par la rivière (voir la figure X(b)). On voit bien sur ce graphe que l'on peut passer de la partie 3 à la partie 4 en utilisant les ponts 5 ou 6. Résoudre le problème d'Euler consiste à trouver un chemin dans le graphe qui passe par toutes les arêtes une fois et une seule : dans le langage des graphes on parlera de chaîne plutôt que de chemin car il n'y a pas d'orientation sur les arêtes.



La ville de KoenigsbergLa ville de Koenigsberg
Figure 1: La ville de Koenigsberg

Bien sûr, Euler a résolu le problème, et vous ?

Peut-on utiliser Metanet pour résoudre ce problème ?

On va tout d'abord créer le graphe en utilisant l'éditeur graphique de Metanet. Pour cela on lance la commande :
metanet()
et la fenêtre graphique s'affiche aussitôt. On clique dans l'item New du menu Files, on donne un nom au graphe, par exemple pont, on dit qu'il n'est pas orienté (en anglais << orienté >> se dit << directed >>) et on utilise la souris pour créer le graphe : un click du bouton droit de la souris crée un sommet, un click du bouton gauche permet de sélectionner un sommet ou une arête, et un click du bouton droit dans un sommet après en avoir sélectionné un autre crée une arête entre les deux. Automatiquement les sommets et les arêtes sont numérotés de façon interne. Il est aussi possible de leur donner n'importe quel nom. Ici cela n'a aucun intérêt et l'on utilise l'item Automatic Name du menu Modify pour utiliser la numérotation interne comme noms. On sauve le graphe (item Save du menu Files ) et par l'item Quit du menu Files on quitte le mode d'édition de graphes. Pour visualiser les noms des sommets et des arcs, on sélectionne les sous-items name des items Node display et Arc display du menu Graph. Voir la figure X. Voilà notre graphe créé, on peut travailler !


La fenêtre Metanet avec le graphe du pont

Figure 2: La fenêtre Metanet avec le graphe du pont

En fait, actuellement, l'éditeur/visualisateur de graphes est un processus indépendant de Scilab et le résultat de notre travail est la création d'un fichier ASCII nommé pont.graph . Il possible que dans des releases futures ce fonctionnement soit changé et que Scilab partage les graphes avec l'éditeur/visualisateur. Donc pour pouvoir travailler avec ce graphe, il faut le charger dans Scilab en faisant :
g=load_graph("pont");
g n'est autre qu'une tlist contenant toutes les informations sur le graphe.

Pour résoudre le problème d'Euler, on va utiliser la méthode bien connue parfois qualifiée de << bourrin >> qui va consister à tester tous les ensembles d'arêtes contenant les 7 arêtes distinctes (qui correspondent aux ponts) afin de voir si ce sont des chaînes. Pour cela, on va d'abord générer tous les arrangements de ces arêtes dans une matrices ar qui va avoir 5040 lignes (factorielle 7) et 7 colonnes : une ligne de la matrice sera un ensemble d'arêtes à tester. Un vrai informaticien de nos amis nous a donné l'algorithme qui permet de générer cette matrice dont nous donnons ci-après la fonction Scilab correspondante (sauvée dans un fichier genar.sci)
function ar=genar(n)
global ar l;
fact=1; for i=1:n, fact=fact*i; end;
ar=zeros(fact,n); l=1;
ar(1,:)=1:n; ref=1:n;
genar1(n,1,ref);

function genar1(n,m,ref)
global ar l;
if size(ref,"*")<>0 then
  for i=1:size(ref,"*")
    ar(l:$,m)=ref(i);
    ref2=ref; ref2(i)=[];
    genar1(n,m+1,ref2);
  end
else
  l=l+1; 
end;
qui génère la matrice désirée. Comme on peut le voir, cette fonction n'a rien à envier à un programme C++ ou Scheme, et elle montre que l'on peut faire de la récursion en Scilab. Nous laissons au lecteur curieux le soin de comprendre pourquoi ça marche, pour notre part nous avons abandonné.

Ensuite la session suivante nous donne la solution du problème des ponts de Koenisberg :
-->getf("genar.sci")
 
-->ar=genar(7);
 
-->nsol=0; sol=zeros(1,7);
 
-->for i=1:size(ar,"r")
-->  p=ar(i,:);
-->  ns=path_2_nodes(p,g); // transforme un chemin en sommets
-->  if ns <> [] then nsol=nsol+1; sol(nsol,:)=p; end;
-->end
 
-->nsol
 nsol  =
    0.  
On boucle sur les lignes de ar qui contiennent toutes les possibilités de chaînes p. La fonction path_2_nodes cherche une suite de sommets consécutifs correspondant à ces chaînes et retourne la matrice vide s'il n'y en a pas. Donc nsol compte le nombre de chaînes qui résolvent le problème et elles sont stockées dans la matrice sol.

Tout cela pour voir qu'il n'y a pas de solution ! C'est d'ailleurs ce qu'Euler avait trouvé. Mais, comme souvent, il vaut mieux réfléchir avant de programmer. À chaque sommet, on peut associer son degré c'est-à-dire le nombre d'arêtes ayant ce sommet pour extrémité. Le fait de pouvoir trouver une chaîne solution du problème est déterminé par le nombre de sommets ayant des degrés pairs ou impairs. Sur la chaîne solution, les sommets intermédiaires figurent une fois s'ils sont de degré 2 (on arrive et on part une et une seule fois) ou 2 fois s'ils sont de degré 4 (chaque passage utilise 2 arêtes), etc. Un sommet de degré impair ne peut donc pas être un sommet intermédiaire, il ne peut être qu'un sommet initial ou terminal. Et dans notre graphe tous les sommets sont de degré impair, donc pas de solution.

Mais il serait dommage d'avoir réalisé ces beaux programmes pour rien. Alors, nous allons les utiliser pour un petit problème bien connu des écoliers (qui s'ennuient en classe). Prenant le dessin d'une enveloppe ouverte, voir la figure X(a), il s'agit de savoir si l'on peut, partant d'un point quelconque, réaliser le tracé sans lever la plume du stylo. La réponse est oui et il est très facile de trouver la solution par quelques essais. On sait même avec le raisonnement de tout à l'heure que la solution part du sommet 1 pour arriver au sommet 2 ou le contraire. Mais une question plus difficile est : combien y-a-t'il de solutions possibles ? On voit sur la même figure X(a) le graphe correspondant au problème qui est ici tout tracé puisque c'est l'enveloppe elle-même (noter que le croisement des diagonales n'est pas un sommet).


Les enveloppes Les enveloppes

Figure 3: Les enveloppes

De même que pour le problème d'Euler, on crée le graphe avec Metanet et on le sauve dans le fichier enveloppe.graph. La session suivante donne le résultat (après un temps de calcul assez long) :
-->getf("genar.sci")
 
-->g=load_graph("enveloppe");
 
-->ar=genar(8);
 
-->nsol=0; sol=zeros(1,8);
 
-->for i=1:size(ar,"r")
-->  p=ar(i,:);
-->  ns=path_2_nodes(p,g);
-->  if ns <> [] then nsol=nsol+1; sol(nsol,:)=p; end;
-->end
 
-->nsol
 nsol  =
    88.  
Il y a 88 possibilités ! Elles sont symétriques deux à deux, donc il y en 44 partant du sommet 1. Tout de même pas facile pour les compter toutes. Par exemple en voici une (la 33 e) trouvée :
-->sol(33,:) // suite des aretes
 ans  =
!   2.    6.    5.    1.    7.    4.    3.    8. !

-->path_2_nodes(ans,g) // suite des sommets
 ans  =
!   2.    3.    5.    1.    2.    5.    4.    3.    1. !
Elle part du sommet 2.

Noter que l'on ne peut pas utiliser cette méthode de recherche exhaustive avec un nombre d'arêtes trop grand car elle est très coûteuse en place mémoire et en temps. Mais elle illustre bien la puissance d'un outil comme Scilab où l'on peut vraiment programmer avec des graphes, c'est-à-dire mélanger les calculs sur les graphes avec la programmation interactive et efficace de Scilab.

On peut se poser la même question dans le cas où l'enveloppe est totalement dépliée (voir la figure X(b)). Comme dans ce cas il y a quatre sommets d'ordre impair égal à 5, il n'y a pas de solution. Notez que ça fonctionne à nouveau si l'on supprime un pli de l'enveloppe complètement dépliée...

On nous a raconté qu'un moniteur de colonie de vacances ne voulant pas donner une autorisation de sortie a trouvé astucieux, plutôt que de choisir un refus mal ressenti, de conditionner la sortie par la solution de ce problème. Les enfants ont usé plusieurs crayons et sont partis se coucher...C'est une application très détournée de la théorie des graphes.

2   Les entrailles de Metanet

Tout d'abord, nous allons théoriser un petit peu et se poser la question fondamentale qui est l'objet du paragraphe suivant.

2.1   Qu'est-ce qu'un graphe ?

Un graphe G=( N, A) est défini par la donnée d'un ensemble de points N qui sont les sommets du graphe (on parle aussi de noeuds) et d'un ensemble A qui sont les arcs (on parle d'arêtes dans le cas où les arcs ne sont pas orientés) du graphe. Si les sommets N sont numérotés (1,2,...,n), un arc est simplement un couple (i,j) avec i,j Î N : il y a donc un lien entre les sommets i et j. On dit que j est la tête (ou le début) de l'arc et i sa queue (ou sa fin). Ceci correspond au cas orienté : l'arc << va de i vers j >> ; dans le cas non orienté, le lien est appelé arête et il n'y pas de sens sur le lien.

Dans le cas de la circulation automobile, par exemple, les liens sont les routes et les croisements sont les noeuds. Quand la route est à double sens entre deux noeuds, il y a un arc dans chaque sens : c'est un graphe orienté. Si l'on considère un réseau téléphonique, entre deux points reliés par une ligne une communication on peut passer dans un sens ou dans l'autre : le graphe (les noeuds sont les centraux) est non orienté. Mais en fait, lorsque l'on veut résoudre des problèmes de télécommunications, les graphes deviennent orientés ; on considère alors un paquet de lignes entre 2 centraux C1 et C2 que l'on ventile à chaque instant (on a un graphe dynamique) : les lignes occupées de C1 vers C2 (donc orientées), celles de C2 vers C1 et enfin les lignes libres (non orientées).

Un graphe peut avoir des boucles, c'est-à-dire des arcs (i,i) et des arcs multiples entre deux sommets (on parle dans ce cas de multigraphes). Au prix de l'augmentation de la taille du graphe, en créant des sommets et des arcs fictifs, on peut se ramener au cas d'un graphe simple, c'est-à-dire qui n'a qu'un seul arc entre deux sommets. Il y a là un point technique très important : en effet les graphes simples sont les seuls à pouvoir être représentés par des matrices (et vice-versa), ce qui permet d'utiliser toute l'algorithmique correspondante qui est bien sûr disponible dans Scilab. Ces matrices ont la particularité d'être creuses (elles contiennent très peu d'éléments non nuls) et les algorithmes spéciaux sur les matrices creuses de Scilab peuvent leur être appliqués.

2.2   La graph-list

Dans Metanet un graphe est représenté par une tlist appelée graph-list et qui contient 33 entrées (dont la plupart sont optionnelles). Seules les cinq premières entrées sont obligatoires :
name
nom du graphe (une chaîne de caractères) ;
directed
flag qui vaut 1 si le graphe est orienté ou 0 sinon ;
node_number
nombre de sommets ;
tail
vecteur ligne des numéros des sommets de queue pour chaque arc ;
head
vecteur ligne des numéros des sommets de tête pour chaque arc ;
Un graphe doit avoir au moins un arc, donc tail et head ne peuvent pas être vides. Avec ce modèle tous les types de graphes, simples, multigraphes, avec ou sans boucle, peuvent être traités par Metanet.


Un petit graphe orienté

Figure 4: Un petit graphe orienté

La fonction de base pour créer un graphe s'appelle make_graph qui prend en entrée les cinq paramètres décrits ci-dessus et génère la tlist du graphe. Par exemple, pour créer le petit graphe orienté de la figure X, on écrira :
g=make_graph("foo",1,4,[1 1 2 3],[2 3 1 3]);
On peut visualiser ce graphe en faisant :
show_graph(g)
qui ouvre automatiquement une fenêtre Metanet s'il n'y en a pas déjà une ouverte et qui affiche le graphe avec un positionnement automatique des sommets puisque nous n'avons pas donné leurs coordonnées.

Bien sûr, on aurait pu créer ce graphe en utilisant l'éditeur graphique comme nous l'avons fait dans le paragraphe X. Dans ce cas, après chargement dans Scilab par load_graph, les coordonnées des sommets sont disponibles dans les slots node_x (vecteur ligne des abscisses des sommets) et node_y (vecteur ligne des ordonnées des sommets). On aurait aussi pu les donner directement dans Scilab par :
g("node_x")=[42 108 176 162];
g("node_y")=[36 134 36 93];
On voit donc que l'on peut faire des allers et retours entre l'éditeur/visualisateur de graphes et l'interpréteur de Scilab et qu'un certain nombre de choses pour la définition et les caractéristiques des graphes peuvent être faites indifféremment dans l'un ou dans l'autre.

En fait, il y a trois catégories d'éléments dans la graph-list : ceux qui définissent la structure mathématique minimale du graphe, ceux qui sont utilisés pour la représentation graphique et ceux qui sont liés à des problèmes de réseaux en donnant des valeurs aux sommets et aux arcs (capacité, longueur, poids, coût, etc.).

Dans ceux qui sont utilisés pour la représentation graphique, node_color et edge_color, qui déterminent respectivement la couleur des sommets et des arêtes, sont très utiles pour visualiser des chemins ou des ensembles de sommets dans les graphes. Les deux fonctions show_arcs et show_nodes permettent aussi de visualiser respectivement des ensembles d'arêtes ou de sommets en les distinguant par rapport aux autres parties du graphe.

2.3   Comment représenter un graphe ?

La structure de données utilisée pour représenter un graphe est capitale pour la complexité des algorithmes : coût et taille. Si la représentation sous la forme tête-queue est très compacte et générale, c'est souvent une représentation par listes d'adjacences, décrite ci-après, qui est utilisée. Elle utilise trois vecteurs (pour un graphe de n sommets et m arcs) : Pour chaque sommet i, la liste des sommets successeurs de i est contenue dans le tableau ls à partir de l'élément numéro lp(i). Les successeurs du sommet i se trouvent donc dans le tableau ls de l'élément lp(i) à l'élément lp(i+1)-1. Par exemple, pour obtenir le vecteur des successeurs du sommet i, on écrit simplement en Scilab ls(lp(i) :lp(i+1)-1). Les arcs correspondants permettant d'atteindre ces sommets successeurs sont dans le tableau la , toujours de l'élément lp(i) à l'élément lp(i+1)-1. Noter que le dernier élément de lp ne correspond pas à un sommet, mais n'est là que pour indiquer la fin des pointeurs.

Dans le cas où le graphe n'est pas orienté, on considère que chaque arête correspond à deux arcs d'orientation opposée.

Par exemple, la représentation pour le graphe de la figure X est :
{short description of image}

La plupart des algorithmes utilisent cette représentation et elle est automatiquement calculée par les fonctions correspondantes. Sinon c'est la fonction adj_lists qui permet de calculer directement ces listes d'adjacence. Toujours avec le même graphe :
-->g=make_graph("foo",1,4,[1 1 2 3],[2 3 1 3]);
 
-->[lp,la,ls]=adj_lists(g)
 ls  = 
!   2.    3.    1.    3. !
 la  =
!   1.    2.    3.    4. !
 lp  =
!   1.    3.    4.    5.    5. !


D'autres représentations sont disponibles dans Metanet : les matrices sommets-arcs, sommets-sommets et les listes chaînées. Avec bien entendu toutes les fonctions qui permettent de les obtenir et de les manipuler.

3   Jouer avec les graphes

Nous allons d'abord montrer en quelques petits exemples comment on peut jouer avec les éléments de la graph-list et visualiser des ensembles de sommets et d'arêtes.

3.1   Distinguer les sommets de degré impair

On peut faire de la programmation en Scilab sur un ou plusieurs graphes afin d'en calculer des caractéristiques comme par exemple distinguer les sommets de degré impair. On écrit ainsi quelques lignes qui vont donner la solution et on va l'appliquer au graphe de l'enveloppe du paragraphe X, figure X(a). D'abord on charge le graphe et on l'affiche :
-->g=load_graph("enveloppe");
 
-->show_graph(g);
On fait une boucle sur les sommets du graphe et on calcule leur degré. find est une fonction standard de Scilab qui prend en argument une condition sur une matrice (expression booléenne) et retourne les indices des éléments de la matrice pour lesquels cette condition est vérifiée. Donc ici find retourne le vecteur des sommets liés au sommet i ; il suffit d'en calculer la taille et, si elle est impaire, de stocker le numéro du sommet correspondant dans le vecteur ns :
-->n=0;
 
-->for i=1:node_number(g)
-->  d=size(find(g('tail')==i | g('head')==i),"c");
-->  if modulo(d,2) <> 0 then n=n+1; ns(1,n)=i; end;
-->end;
On trouve bien les sommets 1 et 2 et on les distingue par show_nodes :
-->ns
 ns  = 
!   1.    2. !
 
-->show_nodes(ns)
Le graphe de l'enveloppe est tout petit, mais le même calcul peut être fait sur un graphe de plusieurs milliers de sommets sans problème. On peut aussi transformer ces lignes de code en fonction Scilab et créer une nouvelle fonction qui retourne pour un graphe donné le nombre de ses sommets de degré impair. En fait, la fonction qui retourne le degré des sommets existe déjà dans Scilab, c'est nodes_degrees, et on aurait pu l'utiliser ici, mais le but était de montrer comment on pouvait facilement programmer sur les graphes avec Scilab.

Sur ce petit exemple, on vient de voir comment le fait de disposer de toute la puissance de Scilab avec la boîte à outils Metanet permet de faire rapidement et facilement des calculs souvent fastidieux avec d'autres logiciels de graphes.

3.2   Circuit hamiltonien

Nous allons montrer maintenant une autre méthode pour distinguer des sommets ou des arcs d'un graphe : faire du coloriage. Nous allons prendre pour cela l'exemple de la recherche d'un circuit hamiltonien.

Un circuit d'un graphe orienté est un chemin fermé, c'est-à-dire une suite d'arcs consécutifs qui permet de partir d'un sommet du graphe pour retourner à ce même sommet. Un circuit hamiltonien est un circuit qui passe une fois et une fois seulement par tous les sommets du graphe. Il n'y a bien sûr aucune raison qu'un cycle et à plus forte raison qu'un cycle hamiltonien existe dans un graphe orienté.

On crée un joli graphe en utilisant l'éditeur de Metanet, on le charge et on cherche un circuit hamiltonien en utilisant la fonction Scilab hamilton qui va retourner en cas d'existence le vecteur des arcs du circuit :
g=load_graph("hamilton");
cir=hamilton(g);
On crée un vecteur de 1 (correspondant à la couleur noire) que l'on remplit de 11 (correspondant à la couleur rouge), on l'assigne à l'entrée edge_color du graphe g et on l'affiche :
edgecolor=ones(1,arc_number(g)); edgecolor(cir)=11*ones(cir);
g("edge_color")=edgecolor;
show_graph(g)
Cela donne la figure X.


Figure 5: Circuit hamiltonien

Figure 5: Circuit hamiltonien

Il est ainsi facile en jouant avec les couleurs de faire apparaître des tas de propriétés ou des résultats de calcul sur les graphes.

3.3   Autres jeux de base

Avec les graphes, on peut faire des tas d'autres choses comme : Toutes ces fonctions, et bien d'autres encore, sont disponibles dans Metanet.

4   Conclusion

Bien sûr il existe toute une panoplie de fonctions plus sophistiquées qui travaillent sur les graphes et les réseaux. Nous avons montré simplement le fonctionnement de la boîte à outils Metanet, mais il y a bien d'autres fonctionnalités car comme disait Hamlet à son pote Horatio << There are more things in heaven and Earth, Horatio, Than are dreamt of in our philosophy >>. Nous verrons cela dans le numéro suivant.

Contacts

Site Web de Scilab : http://www-rocq.inria.fr/scilab

Serveur ftp : ftp://ftp.inria.fr/INRIA/Scilab

Adresse email : Scilab@inria.fr

Newsgroup : comp.soft-sys.math.scilab


This document was translated from LATEX by HEVEA.