Projet d'algorithme
Manipulation de graphes
LIU Florence et MULOT Yannick
Introduction
Le projet consiste à réaliser la commande graphes qui applique différents algorithmes sur les graphes.
Nous présenterons dans une première partie les fichiers sources, la commande et ses différentes options (alogorithmes) ainsi que les formats des fichiers. Dans la deuxième partie nous définirons les structures de données utilisées pour la représentation des graphes dans le code et nous donnerons les étapes pour construire le graphe à partir d'un fichier ou d'un autre graphe. Puis nous détaillerons les algorithmes et leur complexité. Et enfin dans la derniére partie, nous expliquerons l'analyse de la ligne de commande.
Chapter 1 Présentation du programme
1.1 Fichiers sources
Nous avons séparer le programme en plusieurs fichiers pour une meilleure compréhension :
Structures :
-
graphe.h et graphe.c contiennent les structures du graphe et les fonctions sur les graphes : construction, conversion, parcours...
- tas.h et tas.c contiennent la structure du tas et les fonctions qui la manipulent. Le tas est utilisé pour l'un des algorithmes de Dijkstra.
Algorithmes :
-
Dijkstra.h et Dijkstra.c.
- fortement_connexe.h et fortement_connexe.c.
- Kruskal.h et Kruskal.c.
- points_articulation.h et points_articulation.c.
- Prim.h et Prim.c.
- tri_topologique.h et tri_topologique.c.
- Warshall-Floyd.h et Warshal-Floyd.c.
Gestion de la ligne de commande :
-
option.h et option.c contiennent les fonctions qui analysent les options et appellent les fonctions d'algorithme.
- main.c.
Pour compiler le programme, nous avons un fichier Makefile dans le répertoire.
Nous expliquons dans la section suivante comment lancer le programme.
1.2 Commande
Synopsis : graphes -acdDhkpst [fichier] [n] [-o fichier]
Une des options au moins doit obligatoirement être présente. Certaines peuvent être demandées en même temps si elles s'appliquent à un même type de graphe (orienté ou non orienté).
En cas d'absence de fichiers dans la ligne de commande, nous utiliserons l'entrée et la sortie standard.
Remarque :
La fin de la saisie sur l'entrée standard est signalée par un Ctrl D.
1.3 Options
-
-h --help pour obtenir l'aide.
- -o fichier pour écrire le résultat dans le fichier.x avec x la lettre de l'option d'algorithme demandée.
Options d'algorithme :
pour les graphes non orientés
-
-a points d'articulation.
Fichier de sortie de type LISTE.
- -k arbre recouvrant minimal par la methode de Kruskal.
Fichier de sortie de type ARBRE.
- -p arbre recouvrant minimal par la methode de Prim.
Fichier de sortie de type ARBRE.
pour les graphes orientés
-
-c composantes fortement connexes.
Fichier de sortie de type LISTE.
- -d n plus courts chemins ayant pour origine le sommet n par l'algorithme de Dijkstra.
Fichier de sortie de type LISTE.
- -D n plus courts chemins ayant pour origine le sommet n par l'algorithme de Dijkstra avec un tas.
Fichier de sortie de type LISTE.
- -s tri topologique.
Fichier de sortie de type TRI.
- -t plus court chemin entre n'importe qeul couple de sommets par l'algorithme de Warshall-Floyd.
Fichier de sortie de type GRAPHE.
1.4 Formats des fichiers
1.4.1 Format d'entrée
Ces fichiers contiennent la description d'un graphe sous forme de matrice d'adjacence ou de liste d'adjacence.
Nous voulons des graphes valués positivement pour pouvoir appliquer tous nos algorithmes.
Description :
-
première ligne
- : le nombre de sommets
- deuxième ligne
- : une lettre indiquant le type du graphe : o pour graphe orienté n pour graphe non orienté
- troisième ligne
- : une lettre indiquant le type de représentation du graphe : m pour matrice d'adjacence, l pour listes d'adjacence.
- lignes suivantes
- : les lignes de la matrice ou la suite des listes.
-
Représentation par matrice du graphe orienté :
- Représentation par listes d'adjacence du graphe orienté :
les arcs sont donnés sous forme des suite de deux valeurs : coût sommet atteint :
| 3 |
| o |
| l |
| 4 1 |
| 2 0 2 2 |
| 5 0 3 1 |
-
Représentation par matrice du
graphe non orienté.
Nous ne fournissons que la matrice triangulaire supérieure :
- Représentation par listes d'adjacence du graphe non orienté :
un arc n'apparait qu'une fois, associé à son extrémité de plus petit indice :
Le programme ignore les blancs en trop (espace ou tabulation) et crée un graphe à partir de ce fichier.
Erreurs de fichier :
-
Si le nombre de sommets est négatif.
- Si le type du graphe est inconnu.
- Si le type de représentation du graphe est inconnu.
- Si nous n'avons pas de nombres sur les lignes suivantes.
- Si le coût est négatif.
- Si une ligne n'est pas entièrement correcte : s'il n'y a pas assez d'èl`ements pour construire le graphe ou si à la fin de la saisie des èlèments nécessaires il reste encore des éléments sur la ligne.
- Si à la fin de la saisie cpomplète du graphe il reste des èlèments dans le fichier.
- Pour les listes d'adjacences si un sommet n'existe pas.
1.4.2 Formats de sortie
Tous les entiers indiqués sur une même ligne sont séparés par un unique caractère blanc. Toutes les lignes se terminent par \n.
Les fichiers de format GRAPHE
Ce fichier est créé par la fonction de Warshall-Floyd. Le résultat de cet algorithme est un graphe représenté par une matrice d'adjacence. Ce fichier est donc identique à celui decrit précédemment.
Les fichiers de format ARBRE
Ce fichier est créé par les fonctions sur les arbres recouvrant minimums.
-
première ligne
- : le nombre d'arcs
- deuxième ligne
- : le poids de l'arbre
- lignes suivantes
- : sur chaque ligne, un des arcs de l'arbre recouvrant (indiqué par un couple de sommets en ordre croissant), les différents couples étant triés dans l'ordre croissant.
Les fichiers de format LISTE
- Sur chaque ligne, une liste des sommets.
S'il y a plusieurs réponses, les lignes sont triées et rangée dans l'ordre croissant.
Les fichiers de format TRI
-
première ligne
- : les états réalisables au début (sommets sans contraintes, en ordre croissant)
- lignes suivantes
- : sur chaque ligne, la liste des sommets (en ordre croissant) réalisables si ceux de la ligne précédente (et ceux au-dessus) sont réalisés (pour un graphe donné, ce fichier est unique).
Chapter 2 Construction des graphes et fonctions utiles
2.1 Structures de données
2.1.1 Graphe
Un graphe peut être représenté par une liste ou une matrice d'adjacence. Nous avons donc fait une union pour pouvoir utiliser l'une des deux repr{esentations en gardant le même nom de type.
typedef union
{
int ** matrice;
Liste * liste;
}Format;
La structure du graphe comporte toutes les informations fournies par le fichier GRAPHE d'entrée.
typedef struct
{
int nb_sommets;
char type; // oriente ou non
char format; // liste ou matrice d'adjacence
Format adjacence;
}Graphe;
Comme nous connaissons le nombre de sommets du graphe, nous allouons exactement la place nécessaire pour le graphe.
2.1.2 Liste d'adjacence
La liste d'adjacence est représentée par un tableau de nb_sommets listes. Chaque cellule d'une liste représente une arête du graphe. Soit la liste i (i est l'indice du tableau où se trouve cette liste). Chacune de ses cellules indique le sommet et le coût de l'arête (i, sommet).
typedef struct _cellule
{
int cout;
int sommet;
struct _cellule * suivant;
}Cellule, * Liste;
2.1.3 Matrice d'adjacence
La matrice d'adjacence est représentée par un tableau à deux dimensions :
- une matrice carrée de taille nb_sommets*nb_sommets pour le graphe orienté.
- une matrice triangualire inférieur de nb_sommets lignes pour le graphe non orienté (à chaque ligne nous diminuons le nombre de colonnes).
Remarque :
Pour un graphe non orienté nous devrions avoir une matrice triangulaire supérieur. Nous avons donc un décalage d'indice dans notre représentation. Le calcul est toutefois simple : sommet = i + j avec i le numéro de la ligne et j le numéro de la colonne de notre matrice.
2.1.4 Constantes
Dans notre programme nous avons défini plusieurs constantes pour indiquer certains aspects d'un graphe.
-
Le type Booleen (par une énumération) qui indique si un sommet à été visité ou non, si c'est un point d'articulation...
- La constante INFINI pour le coût d'un chemin ou d'une arête qui n'existe pas dans le graphe ou pour leur initialisation. Ici nous avons INFINI qui vaut 500.
- La constante AUCUN pour indiquer qu'un sommet n'a pas de prédecesseur ou tout simplement qu'une variable n'a pas de valeur (initialisation).
2.2 Construction du graphe à partir d'un fichier
Nous avons deux types de graphe (orienté et non orienté) et deux représentation (liste et matrice d'adjacence). Il nous faut donc distinguer quatre constructions possibles.
De plus pour faciliter par la suite l'application de certains algorithmes (comme Dijkstra) nous vérifions que le graphe est valué positivement.
2.2.1 Fichier2graphe
La fonction fichier2graphe vérifie les trois premières lignes du fichier (erreur si format ou type inconnu, nombre de sommets négatif ou nul), remplit les champs de la structure Graphe et appelle une des fonctions de constructions du graphe en fonction du type et du format.
Arguments :
-
fichier d'entrée.
- adresse du graphe à construire.
Valeur de retour :
-
-1 s'il y a une erreur dans les trois première ligne du fichier, nous n'avons pas à libérer le graphe puisque sa construction n'a pas commencé.
- 0 s'il y a une erreur dans le graphe (problème de mmoire ou erreur dans le fichier). Il faut que la fonction appelante libère le graphe.
- 1 si le graphe est construit.
Complexité :
La complexité de cette fonction dépend de celle de la fonction de construction appelée. Elle est soit en 0(n) avec n le nombre d'arcs soit en O(n2) avec n le nombre de sommets.
2.2.2 Liste d'adjacence
Nous avons les fonctions graphe_o_liste et graphe_n_liste qui fabriquent une liste d'adjacence.
Arguments :
-
fichier d'entrée.
- adresse du graphe à construire.
Valeur de retour :
-
0 s'il y a une erreur dans le graphe (problème de mmoire ou erreur dans le fichier).
- 1 si le graphe est construit.
Ces deux fonctions sont pratiquement identiques à quelques conditions près.
Algorithme :
Nous allouons un tableau de listes de taille nb_sommets (allocation avec calloc pour initialiser les listes à NULL).
De i variant de 1 à nb_sommets :
-
Nous lisons une ligne du fichier. Cette ligne doit contenir un nombre pair de nombres (coût et sommet).
-
Si cette ligne est vide on passe à la liste suivante.
- Sinon nous vérifions pour chaque couple que le coût est strictement positif et que le sommet existe (compris entre 0 et nb_sommets-1). Nous allouons une cellule en lui affectant le coût et le sommet à ses champs et nous l'ajoutons à la liste.
Nous vérifions aussi qu'il n'y ait rien à la fin du fichier.
Pour le graphe non orienté nous regardons en plus que le sommet lu dans la boucle ne soit pas supérieur à i (numéro de l'autre sommet de l'arète).
Complexité :
Les tests, les affectations, les allocations sont des opérations constantes. Nous parcourons toutes les lignes du fichier. Au mieux, nous avons une complexité en O(n) avec n le nombre de sommets dans le graphe. Au pire nous avons également une complexité en O(n) mais avec n le nombres d'arcs dans le graphe.
2.2.3 Matrice d'adjacence
Nous avons les fonctions graphe_o_matrice et graphe_n_matrice qui construisent une matrice d'adjacence.
Arguments :
-
fichier d'entrée.
- adresse du graphe à construire.
Valeur de retour :
-
0 s'il y a une erreur dans le graphe (problème de mmoire ou erreur dans le fichier).
- 1 si le graphe est construit.
Ces deux fonctions sont quasiment identiques. La seule différence est que pour le graphe non orienté nous avons une variable qui diminue le nombre de colonnes à chaque entrée de la boucle pour finalement obtenir une matrice triangulaire.
Algorithme :
Nous allouons un tableau de tableaux de taille nb_sommets.
De i variant de 1 à nb_sommets :
-
Nous allouons les lignes de la matrice.
Nous lisons une ligne du fichier.
- De j variant de 1 à la taille de la ligne allouée :
-
nous récupérons le coût de l'arète (i, j) (nous vérifions que le coût ne soit pas négatif).
- Nous regardons qu'il n'y ait plus rien sur la ligne du fichier.
Nous vérifions aussi qu'il n'y ait rien à la fin du fichier.
Complexité :
Les tests, les affectations, les allocations sont des opérations constantes. Nous parcourons le fichier pour remplir toute la matrice donc la complexité est en 0(n2) avec n le nombre de sommets dans le graphe.
2.3 Conversion du graphe d'un format à l'autre
Certains algorithmes sur les graphes fonctionnent plus rapidement ou sont plus facile à écrire avec un type de représentation en mémoire particulier. De ce fait il nous est utile de pouvoir convertir les formats des graphes. Ces conversions ne sont pas difficiles à faire étant donné auà ce stade les coûts et les sommets sont corrects. Le seul problème qui peut se poser c'est celui de la place mémoire.
Les fonctions de conversion prennents les mêmes arguments et retournent les mêmes valeurs.
Arguments :
-
graphe original (source).
- adresse du graphe converti (destination).
Valeur de retour :
-
0 s'il y a un problème de mmoire.
- 1 si le graphe à été converti.
A la fin de ces fonctions le graphe source existe toujours. Il faut le libérer dans la fonction qui appelle une des fonctions de conversion puisque nous n'en avons plus besoin.
2.3.1 D'une matrice à une liste
La fonction matrice2liste affecte aux champs du graphe destination ceux du graphe source en changeant évidemment le champs format. Elle alloue le tableau de listes et appelle une des deux fonctions de conversion qui suivent en fonction du type du graphe.
Les fonctions o_matrice2liste et n_matrice2liste se ressemblent beaucoup. Pour le graphe non orienté les indices de la matrice triangulaire sont décalés, il faut y faire attention.
Algorithme :
Pour chaque case de la matrice, si le coût n'est pas nul nous ajoutons le sommet et le coût dans la liste correspondante.
Complexité :
Les tests, les affectations, les allocations sont des opérations constantes. Nous parcourons toute la matrice donc la complexité est en 0(n2) avec n le nombre de sommets dans le graphe.
2.3.2 D'une liste à une matrice
La fonction liste2matrice remplit les champs du graphe destination avec ceux du graphe source en modifiant le champs format. Elle alloue le tableau de tableaux (de int) et appelle une des deux fonctions de conversion qui suivent en fonction du type du graphe.
Les fonctions o_liste2matrice et n_liste2matrice sont assez simples. Pour le graphe non orienté nous avons une variable qui diminue pour obtenir la matrice triangulaire.
Algorithme :
Pour chaque ligne de la matrice, nous allouons la ligne (allocation avec calloc pour initialiser les cases à 0). Nous parcourons la liste correspondante à la ligne et pourélément de la liste, nous récupérons le coût et nous l'affectons au sommet correspondant.
Complexité :
Nous parcourons toutes les listes. Chaque cellule représente un arc donc la complexité est en 0(n) avec n le nombre de sommets ou d'arcs dans le graphe.
2.4 Fonctions diverses sur les graphes
2.4.1 Libération du graphe
Si le graphe est représenté par une liste d'adjacence, nous libérons chaque liste par la fonction récursive liberer_liste puis nous libérons le tableau de listes. La complexité est donc en O(n) avec n le nombre de sommets ou d'arcs dans le graphe
Si le graphe est une matrice d'adjacence, nous libérons chacune de ses lignes puis le tableau. La complexité est en O(n) avec n le nombre de sommets.
2.4.2 Connexité du graphe
Pour appliquer les algorithmes sur les graphes non orientés il faut que les graphes soient connexes, qu'il soit en un seul morceau.
Pour vérifier cela nous eesayons de voir si nous pouvons atteindre tous les sommets par un seul parcours.
Comme nous voulons regarder dès le début si le graphe est connexe nous avons écrit deux fonctions de parcours, un pour une représentation en liste d'adjacence, un pour une représentation en matrice.
Les fonctions parcours_liste_prefixe et parcours_matrice_prefixe parcourent le graphe et comptent le nombre de sommets visités.
La fonction graphe_connexe appelle l'une des deux fonctions précédentes en fonction du format du graphe et compare le nombre de sommets visités avec le nombre de sommets total dans le graphe. Elle retourne 1 si le graphe est connexe, 0 sinon. La complexité de cette fonction dépend de la complexité de la fonction appelée, c'est-à-dire en O(n) pour une liste avec n le nombre d'arcs et en O(n2) pour une matrice avec n le nombre de sommets.
Chapter 3 Algorithmes sur les graphes
3.1 Graphe non orienté
A l'exécution des alogorithmes sur les graphes non orientés, nous supposons que le graphe passé en argument est bien non orienté et connexe.
3.1.1 Algorithme de Kruskal
La fonction Kruskal construit l'arbre couvrant minimal d'un graphe représenté par une liste d'adjacence. Pour cela nous essayons de construire un arbre avec nb_sommets-1 arcs sans cycle. Comme le graphe est connexe, nous pouvons construire cet arbre.
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Valeur de retour :
-
0 si l'arbre ne peut pas être construit (problème de mémoire).
- 1 si l'arbre est écrit dans le fichier.
Structures de données :
-
L'arbre est reprsenté par une matrice triangulaire. Cette représentation facilite l'affichage en ordre croissant des arêtes.
- Toutes les arêtes du graphe sont rangées dans un tableau. Le nombre d'aretes maximum dans le graphe est de :
| max_aretes |
= |
|
i = nb_sommets*(nb_sommets+1)/2 |
Ce tableau contient les numéros de sommet des deux extrémités de l'arête (le plus petit numéro avant) ainsi que son coût.
Algorithme :
Au début nous considérons que chaque sommet constitue un sous-arbre et le poids total de l'arbre recouvrant est initialisé à 0.
Tant que nous n'avons pas les nb_sommets-1 arêtes de l'arbre :
-
Nous cherchons l'arête de poids minimal et nous le supprimons de l'ensemble d'arêtes.
- Si les deux extrémités de l'arête ne sont pas dans le même sous-arbre, nous pouvons le rajouter à l'arbre sans créer de cycle et fusionner les deux sous-arbres. Le poids de l'arbre recouvrant est augmenté du coût de l'arête.
Nous ecrivons dans le fichier le nombre d'arcs, le poids de l'arbre et les arcs (couple de sommets) en parcourant la matrice triangulaire.
La fonction Kruskal utilise la fonction fusionner_arbres pour relier les deux sous-arbres. Cette fonction remet à jour le le tableau numero qui indique le numéro du sous-arbre auquel appartient un sommet donc sa complexité est en O(n) avec n la taille du tableau soit nb_sommets.
Complexité :
Nous cherchons nb_sommets-1 arêtes parmi nb_aretes. Donc au pire nous avons une complexité en O(nb_sommets*max_aretes) soit en O(n3) avec n le nombre de sommets. Si le graphe n'a pas beaucoup d'arcs nous avons une complexité en O(n2) avec n le nombre de sommets car nous appellons nb_sommets la fonction fusionner_arbres qui est en O(n).
3.1.2 Algorithme de Prim
L'algorithme de Prim permet également de construire un arbre couvrant minimal mais d'un graphe repésenté par un matrice triangulaire (il faut faire attention aux indices).
Arguments :
-
le graphe à étudier.
- un sommet origine de la construction (racine).
- le fichier de sortie.
Valeur de retour :
-
0 si l'arbre ne peut pas être construit (problème de mémoire).
- 1 si l'arbre est écrit dans le fichier.
A la différence de la fonction Kruskal qui construisait plusieurs sous-arbres, la fonction Prim ne construit qu'un seul arbre. Elle y rajoute les sommets un par un. Le graphe étant connexe, tous les sommets du graphe seront ajouteés dans l'arbre.
Nous distinguons deux ensembles de sommets dans cet algorithme :
-
sommets de l'arbre couvrant.
- sommets non traités (en dehors de l'arbre).
Structures de données :
-
l'arbre est représenté par une matrice triangulaire aussi.
- le tableau plus_proche indique le sommet le plus proche d'un autre.
- le tableau distance donne le plus petit cout entre un sommet de l'arbre et un sommet non traité.
Algorithme :
Nous considérons le sommet passé en paramtre comme le premier sommet de l'arbre. Nous initialisons donc le tableau plus_proche par ce sommet. Le tableau distance est donc initialisé par la ligne racine de la matrice triangulaire.
Tant qu'il y a encore des sommets en dehors de l'arbre :
-
Nous cherchons le sommet non traité le plus proche de l'arbre. Soit s ce sommet.
- Nous marquons le sommet comme traité et nous ajoutons l'arête (s, plus_proche[s]) dans l'arbre couvrant.
- Pour tous les sommets u non marqués adjacents au dernier sommet traité (remise à jour des tableaux distance et plus_proche) :
-
Si le coût entre le sommet adjacent u et le sommet traité s est inférieur à distance[u] alors nous avons un nouveau plus petit coût entre un sommet de l'arbre et un sommet non traité. Nous affectons à distance[u] ce nouveau plus petit coût et á plus_proche[u] le sommet s.
Nous ecrivons dans le fichier le nombre d'arêtes, le poids de l'arbre et les arêtess (couple de sommets) en parcourant la matrice triangulaire.
Complexité :
Nous recherchons le sommet s par parcours du tableau distance et nous effectuons ce parcours nb_sommets fois pour traiter tous les sommets du graphe. Donc la complexité de la fonction Prim est en O(n2) avec n le nombre de sommets dans le graphe.
3.1.3 Points d'articulation
Un sommet s est un point d'articulation du graphe G connexe si G privé de s n'est plus connexe.
Nous notons num[s] le numéro du sommet s. Pour chaque sommet s nous définissons bas[s] comme le plus petit numérod'un sommet t tel qu'il existe une arête (x, t) qui n'est pas dans l'arbre d'exploration A avec
num[t] £ num[s] £ num[x]
x est un descendant de s dans A (l'arc (x, t) est un arc arrière).
Si une telle arête n'existe pas nous posons bas[s]=num[s].
A partir de là, un sommet s est un point d'articulation si et seulement si:
-
s est la racine de l'arbre d'exploration A et a au moins deux fils dans A.
- s a un fils t dans A tel que bas[t] ³ num[s]
Nous avons notre algorithme.
Nous travaillons sur un graphe représenté par une liste d'adjacence.
Décrivons d'abord la fonction calculer_num_bas qui, comme l'indique son nom, calcule les valeurs des tableaux num et bas.
Arguments :
-
le graphe à étudier.
- le sommet où nous nous trouvons dans l'exploration du graphe.
- le tableau num.
- le tableau bas.
- le tableau pere qui indique le père d'un sommet dans l'arbre d'exploration.
- le pointeur d'une variable de type entier incrémentée à chaque appel de la fonction pour numéroter les sommets.
Algorithme :
Soit s le sommet à traiter, nous initialisons num[s] et bas[s] à l'entier passé en paramètre. Nous incémentons cette variable.
Pour tous les successeurs t du sommet s dans le graphe :
-
Si le sommet t n'a pas encore été visité (num[t]==-1), nous indiquons que s est son père dans l'arbre d'exploration et nous rappelons la fonction pour traiter ce sommet.
- Sinon (s, t) est un arc arrière. Le graphe est non orienté donc un arc n'apparait qu'une fois dans la liste d'adjacence, associé à son extrémité de plus petit indice. Par conséquent num[s] est forc ément plus petit que num[t]. Nous modifions tous les bas à partir de t en remontant dans l'arbre jusqu'au sommet s en leur affectant la valeur de num[s].
Complexité :
Cette fonction est récursive et s'appelle nb_sommets fois pour traiter tous les sommets du graphe (le graphe étant connexe tous les sommets seront traités). Pour chaque sommet nous regardons tous leurs successeurs et modifions le tableau bas (de taille nb_sommets). Si nous changeons à chaque appel toutes les valeurs du tableau bas, nous pouvons estimer au pire des cas la complexité à O(kn) avec k le nombre de sommets dans le graphe et n le nombre d'arêtes.
Pour calculer les points d'articulation d'un graphe il faut que ce graphe ait au moins trois sommets. Cette vérification est faite dans la fonction points_articulation.
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Valeur de retour :
-
0 s'il y a moins de trois sommets.
- 1 si le fichier est rempli.
Algorithme :
Nous initialisons tous les tableaux à -1 et nous appelons la fonction calculer_num_bas pour le sommet 0.
Nous comparons les valeurs des tableaux num et bas, comme nous l'avons indiqué précédemment, à l'aide le tableau pere pour connaitre les fils des sommets (nous ignorons donc les sommets sans fils).
Nous écrivons les sommets qui sont des points d'articulation dans le fichier dans l'ordre croissant.
Complexité :
Les comparaisons et affectations sont des opérations constantes. Donc la comparaison des valeurs des tableaux se font en O(nb_sommets). S'ajoute la complexité de la fonction calculer_num_bas. Donc la complexité de la fonction poitns_articulation est en O(kn) avec k le nombre de sommets dans le graphe et n le nombre d'arêtes.
3.2 Graphe orienté
A l'exécution des alogorithmes sur les graphes orientés, nous supposons que le graphe passé en argument est bien orienté.
3.2.1 Algorithme de Dijkstra
L'algorithme de Dijkstra consiste à trouver tous les plus courts chemins ayant pour origine un sommet donné. Cet algorithme ne s'applique qu'à des graphes valués positivement. Nous l'appliquons à un graphe représenté par une liste d'adjacence.
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Structures de données :
-
le tableau distance qui contient la valeur de la plus petite distance entre l'origine et un sommet.
- le tableau predecesseur qui donne le prédecesseur d'un sommet dans le plus court des chemins.
Par la suite nous appellerons S l'ensemble des sommets i pour lesquels la longueur optimale du plus court chemin de l'origine s à i est connue et E som complémentaire.
Algorithme :
Nous initialisons le tableau distance à ¥ (nous aurions pu initialiser distance[i] au coût de l'arête (s, i mais cela revient au même de le faire plus tard dans la boucle). Nous mettons distance[s] à 0.
Tant qu'il y a au moins un sommet dans E :
-
Nous cherchons le sommet x de E tel que distance[x] est minimum.
- Si distance[x] vaut ¥, les sommets restants de l'ensemble E sont inaccessibles depuis l'origine. Nous sortons de la boucle.
- Nous rajoutons x dans l'ensemble S
- Pour tout sommet u adjacent à x
-
Si la distance entre l'origine et u est plus court en passant par x, nous l'affectons à distance[u]. Nous remettons également à jour predecesseur[u].
Pour tous les sommets [i] du graphe
-
S'il n'y a pas de chemin allant de l'origine au sommet i nous écrivons une ligne vide dans le fichier.
- Sinon nous écrivons la plus courte distance entre l'origine s et le sommet i suivi de la suite des sommets intermédiaire sur le plus court chemin. Cette suite est obtenue à l'aide du tableau predecesseur.
Complexité :
Nous cherchons le plus court chemin d'un sommet donné vers les autres sommets donc nous calculons nb_sommets chemins.
Pour chaque chemin nous cherchons la plus petite distance parmi les sommets non traités. La complexité de la fonction Dijkstra est en O(n2) avec n le nombre de sommets dans le graphe.
3.2.2 Algorithme de Dijkstra avec un tas
Plusieurs algorithmes ont été développés pour améliorer la complexité de l'algorithme de Dijkstra notamment en introduisant une structure de tas pour choisir le sommet i entrant dans l'ensemble S à chaque étape.
Structure du tas :
Nous repésentons le tas par un tableau. Chaque élément du tableau contient un sommet et la distance du chemin allant de l'origine à ce sommet.
typedef struct
{
int sommet;
int distance;
}Element;
typedef struct
{
Element * tableau;
int nb_elements;
}Tas;
Nous commençons le tableau à 1 au lieu de 0 pour faciliter les calculs. Par conséquent tas.tableau[1] est la racine.
Nous considérons le tas comme un arbre binaire donc le sommet tas.tableau[i] du tas :
-
a pour fils gauche tas.tableau[2*i].
- a pour fils droit tas.tableau[2*i+1].
- a pour pere tas.tableau[i/2].
- est une feuille ssi 2*i > tas.nb_elements.
Nous avons les fonctions :
-
creer_tas qui alloue le tableau à une taille donnée en paramètre.
- detruire_tas qui libère le tableau.
- est_vide qui indique si le tas est vide ou non.
- echanger qui échange les valeurs des deux éléments de l'arbre.
- ajouter_element qui rajoute l'élément à la dernière feuille de l'arbre (à la fin du tableau).
- prendre_minimum qui prend et supprime la racine de l'arbre et réorganise l'arbre.
- echanger_element qui change la valeur d'un élément eet réorganise le tas.
Nous allons voir plus en détails la réorganisation du tas dans les trois dernière fonctions. Nous avons ici trois réorganisation :
-
partant d'une feuille (fin du tableau) : nous effectuons les echanges tant que l'element est inferieur a son pere (ou jusqu'a la racine).
- partant de la racine (début du tableau) :
tant que tas.tableau[i] n'est pas une feuille,
-
nous cherchons le plus petit des deux fils de tas.tableau[i].
- nous echangeons si le fils est plus petit que le pere.
- partant d'un noeud (n'importe où dans le tableau) : dans echanger_element la nouvelle valeur est plus petite que l'ancienne donc nous reorganisons le tas en partant du sommet modifié et nous remontons jusqu'à ce qu'il soit à sa place (au pire à la place de la racine).
L'algorithme de Dijkstra :
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Valeur de retour :
-
0 si le tas ne peut pas être créé.
- 1 si le fichier a été rempli.
Algorithme :
Nous initialisons les tableaux et crééons le tas. Nous y ajoutons l'origine s avec comme distance 0.
Tant que le tas n'est pas vide :
-
Nous prenons le sommet x du tas tel que distance[x] soit minimum.
- Pour tous les sommets u adjacents à x, nous remmettons à jour distance[u] et predecesseur[u]. Si le sommet n'existe pas dans le tas, nous l'y ajoutons sinon nous ne changeons que sa valeur distance.
Nous détruisons le tas.
Nous écrivons les résultats dans le fichier.
Complexité :
Avec le tas, la complexité de l'algorithme est amélioré. La fonction Dijkstra_tas est en O(nlogn).
3.2.3 Algorithme de Warshall-Floyd
L'algorithme de Warshall-Floyd donne les plus courts chemins entre n'importe quel couples de sommets. Nous l'appliquons sur un graphe représenté par une matrice d'adjacence.
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Structures de données :
-
une matrice cout contenant le cout des plus courts chemins
- une matrice pere contenant le pere des sommets dans les plus courts chemins (inutile pour nous ici)
Algorithme :
Nous recopions la matrice d'adjacence dans la matrice cout. S'il n'y a pas d'arêtes entre deux sommets nous mettons le coût à l'infini.
Pour k variant de 0 à nb_sommets-1 :
-
Pour i variant de 0 à nb_sommets-1 :
-
Pour j variant de 0 à nb_sommets-1 :
-
Si la distance du chemin pour aller de i à j en passant par k est plus petite que le chemin précedent, nous remettons à jour distance[i][j] et predecesseur[i][j].
Nous recopions la matrice distance dans le fichier.
Complexité :
Nous cherchons un chemin pour chaque couple de sommets en passant par chaque sommet. Nous avons donc une complexité en O(n3) avec n le nombre de sommets dans le graphe.
3.2.4 Composantes fortement connexes
Un graphe est fortement connexe si pour tous sommets u et v il existe un chemin de u à v et de v à u. Un sous-graphe fortement connexe ayant un nombre maximal de sommet est appelé composante fortement connexe.Nous allons déterminer ces composantes à partir d'un graphe repésenté par une matrice.
Le choix de la représentation du graphe est important car nous devons parcourir le graphe transposé (les arcs (i,j) deviennent des arcs (j, i) ). Nous n'aurons pas à transformer le graphe car sa transposée est facile à obtenir par la matrice.
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Algorithme :
Nous effectuons un premier parcours en profondeur du graphe en notant les sommets dans un tableau à la fin de leur exploration. S'il y a encore des sommets non visités nous reprenons le parcours sur le plus petit sommet.
Nous effectuons un deuxième parcours en profondeur mais sur le graphe retourné cette fois. Nous faisons un parcours en préfixe avec comme sommet de départ le dernier sommet du tableau obtenu par le premier parcours. S'il y a encore des sommets non visités nous recommençons le parcours avec le dernier sommet non visité du tableau. Lors de ce deuxième parcours nous indiquons à quelle composante connexe appartient chaque sommet par un numéro.
Nous écrivons chaque composante connexe dans le fichier dans l'ordre croissant des sommets.
Complexité :
Nous faisons plusieurs parcours en profondeur. Le parcours en profondeur s'effectue en O(n) avec n le nombre de sommets. A la fin des parcours nous obtenons un tableau indiquant pour chaque sommet à quelle composante connexe il fait partie. Pour afficher les composantes trièes dans l'ordre nous devons parcourir ce tableau autant de fois qu'il y a de composantes fortement connexes. Donc au pire des cas nous avons une complexité en O(n2) avec n le nombre de sommets dans le graphe.
3.2.5 Tri topologique
Le tri topologique indique l'ordre dans lequel les états (sommets) d'un graphe peuvent se réaliser en sachant que certains sont réalisable en même temps (nous les trions dans ce cas là).
Arguments :
-
le graphe à étudier.
- le fichier de sortie.
Valeur de retour :
-
0 si le graphe possède un cycle, il est impossible de faire le tri.
- 1 si le fichier est rempli.
Comme nous souhaitons les suites de sommets dans l'ordre croissant nous n'utiliserons pas un ensemble S pour les sommets non traités (comme dans Dijkstra) qui risquent de les mélanger mais plutôt un marqueur. Nous choisissons donc de parcourir le tableau predecesseur (qui indique le nombre de prédécesseurs pour un sommet) plusieurs fois ce qui implique une complexité pouvant aller jusqu'à O(n2).
Algorithme :
Nous indiquons dans un tableau predecesseur le nombre de prédècesseurs pour chaque sommet.
Tant que nous n'aurons pas traité tous les sommets :
-
Nous cherchons dans le tableau predecesseurs les sommets qui n'ont pas de prédécesseurs et nous les affichons au fur et à mesure.
- Si nous n'en avons trouvé aucun alors il y a un cycle dans le graphe, nous sortons de la fonction.
- Sinon nous marquons ces sommets et supprimons les arcs qui leur sont associés.
Complexité :
Nous parcourons le tableau predecesseur de taille nb_sommets autant de fois qu'il y a d'étapes dans le tri topologique. Dans le pire des cas si nous n'avons qu'un sommet par étape nous aurons nb_sommets étapes et donc nous aurons une complexité en O(n2).
Chapter 4 Gestion de la ligne de commande
La gestion de la ligne de commande se fait en grande partie dans la fonction principale main. Comme les éléments de la ligne de commande se trouvent dans les arguments du main il est inutile d'écrire une fonction avec comme paramètres ceux de main. Toutefois la reconnaissance des options et l'appel des fonctions d'algorithme sont faites par des fonctions définies dans les fichiers option.h et option.c.
4.1 Analyse de la ligne de commande
La ligne de commande contient au moins une option. Les noms des fichiers sont facultatifs.
Nous vérifions le nombre d'arguments sur la ligne de commande. Il faut :
-
au moins deux arguments
-
le nom du l'exécutable.
- une option.
- au plus six arguments
-
le nom de l'exécutable.
- les options.
- un numéro de sommet pour l'algorithme de Dijkstra.
- le nom du fichier d'entrée.
- l'option -o suivi du nom du fichier de sortie.
- le nom du fichier de sortie.
4.1.1 Analyse des options
Nous analysons d'abord l'argument des options qui doit être en deuxième position sur la ligne de commande. Nous indiquons à l'aide d'un tableau de type Booleen quelles options sont demandées. Cette analyse est faite par la fonction reconnaitre_options :
Arguments :
-
la chaine de caractères contenant les options.
- le tableau options.
- l'adresse d'un char indiquant à quel type de graphe s'applique les options demandées.
Valeur de retour :
-
0 s'il y a une erreur (options non compatibles, options inconnues...).
- 1 si la chaine est correcte et que la reconnaissance des options est faite.
Algorithme :
Nous initialisons le tableau options à FAUX pour indiquer qu'aucune option n'est demandée.
La chaîne doit commencer par le caractère -et avoir une lettre à la deuxième position.
Pour tous les caractères de cette chaîne, nous regardons si l'option existe :
-
Si l'option est compatible avec celles déjà demandées (nous vérifions qu'elle s'applique au même type de graphe que les autres).
- Si elle n'a pas déjà été demandée.
Complexité :
Nous parcourons toutes la chaines pour reconnaitre les options. Donc la complexité est en O(n) avec n la longueur de la chaine.
Nous avons ainsi le tableau des options, il nous reste plus qu'à le parcourir et exécuter les algorithmes.
4.1.2 Analyse des autres arguments
Les arguments autres que le nom de l'excutable et les options n'ont pas de place précise sur la ligne de commande. Chaque argument ne doit être présent qu'une seule fois sur la ligne de commande.
Nous considérons qu'un nom de fichier ne peux pas être un nombre et que si nous avons un nombre sur la ligne de commande alors il correspond à un sommet.
Après avoir sauvé ces arguments dans des variables, nous les analysons :
-
Si nous avons un numèro de sommet, nous regardons si l'un des algorithmes de Dijkstra (qui nécessitent un sommet) est demandé. Si non il y a une erreur dans la ligne de commande. De même si l'un des algorithmes est demandé et que le sommet n'est pas fourni.
- Si nous avons un nom de fichier d'entrée (sans l'option -o devant qui indiquerait que c'est un fichier de sortie), nous essayons de l'ouvrir. En cas d'erreur nous sortons du programme.
Enfin nous pouvons créer le graphe et appliquer nos algorithmes.
4.2 Exécution du programme
4.2.1 Création du graphe
Le graphe est créé à partir du fichier d'entrée. Si ce fichier n'existe pas nous utilisons l'entrée standard.
Nous appelons la fonction fichier2graphe pour créer le graphe. S'il y a une erreur lors de la construction du graphe nous le libérons et sortons du programme.
Nous vérifions que les options demandées sont appliquables au graphe construit.
4.2.2 Exécution des algorithmes
Nous avons deux cas à distinguer : les graphes orientés et les graphes non orientés.
Pour chacun des cas, L'ordre d'exécution des algorithmes dépend du format du graphe.
Par exemple, si nous avons un graphe représenté par une matrice nous effectuons d'abord les algorithmes qui s'appliquent sur les matrices d'adjacence puis nous convertissons le graphe en liste d'adjacence pour pouvoir y aplliquer les autres algorithmes.
Voici donc une partie de la fonction main concernant l'exécution des algorithmes :
-
Si le graphe créé est non orienté, nous testons sa connexité.
-
Si le graphe est une matrice d'adjacence :
-
Nous appelons les fonctions sur les graphe non orientés reprénsentés par une matrice.
- Nous convertissons le graphe en liste d'adjacence.
- Nous appelons les fonctions sur les graphes non orientés représentés par une liste.
- Sinon nous effectuons les mêmes opérations dans l'autre sens.
- Si le graphe est orienté : l'algorithme de Dijkstra fait parti des algorithmes sur les graphes orientés, il faut donc encore tester le sommet passé en argument. Il faut vérifier que le numéro du sommet soit inférieur au nombre de sommets dans le graphe (dans le cas où il n'y pas de sommet, nous avons initialisé le sommet à -1 donc rend le test vrai).
Nous effectuons les mêmes opérations que pour le graphe non orienté en appelant dans ce cas là les fonctions sur les graphes orientés.
Pour ne pas surcharger la fonction main, nous avons écrit quatre fonctions qui regroupent les algorithmes selon leur type et format :
-
algo_n_matrice
- algo_n_liste
- algo_o_matrice
- algo_o_liste
Ces fonctions ont à peu près le même prototype et le même algorithme :
Arguments :
-
le graphe étudié.
- un sommet (seulement pour la fonction algo_o_liste qui appelle la fonction Dijkstra et donc nécessite un sommet).
- le tableau options (pour savoir quel algorithme exécuter).
- le début du nom du fichier de sortie.
Algorithme :
Le fichier de sortie est initialisé à la sortie standard.
Pour chaque algorithme :
-
S'il y a un nom de fichier, nous y ajoutons l'extension et nous ouvrons le fichier correspondant.
-
Si il y a un erreur dans l'ouverture du fichier, nous passons à l'algorithme suivant.
- Nous appelons la fonction correpondant à l'algorithme.
La seule erreur que nous pouvons rencontrer dans l'appel de ces fonctions est l'impossibilité de créer les fichiers de sortie. Cela peut être un problème de droits donc si nous avons une erreur pour l'un des fichiers de sortie, le programme ne s'arrête pas et passe à l'algorithme suivant. Ainsi nous exécutons le plus d'algorithmes possibles.
Complexité :
La complexité de ces fonctions dépendent de celle des fonctions appelées.
This document was translated from LATEX by
HEVEA.