Remarque : Il est possible d'affecter des valeurs, des poids (coefficients positifs), des couleurs, etc... aux arêtes (le plus souvent) ou bien aux sommets (parfois) : on parle de graphe valué. Ces valeurs peuvent indiquer des distances, des coûts, etc. Pour être plus précis, on peut utiliser les termes graphes pondérés pour un graphe valué par des nombres et graphe étiqueté pour un graphe valué par des chaînes de caractères.
Un peu de vocabulaire pour fixer ces notions :
Ce qui compte, c'est de se promener sur un graphe... :
Dans un graphe connexe :
Pour un graphe donné, on associe à chaque sommet
la liste de ses voisins (sommets adjacents). Dans le cas
d'un graphe orienté, on peut aussi parler de «liste de successeurs»
(flèches partant du sommet considéré) ; dans le cas orienté, on peut aussi
considérer la «liste de prédécesseurs» (flèches arrivant au sommet
considéré).
Lorsque le graphe est valué, on associe à chaque sommet une liste de
tuples (voisin, dictionnaire), où les clés du dictionnaire (poids,
couleur) permettent d'accéder à la/les donnée(s) figurant sur l'arête. Si
on utilise une seule donnée, le dictionnaire n'est pas nécessaire : on le
remplace dans le tuple directement par cette seule donnée.
{A:[B,D,E], B:[A,E], C:[D,E], D:[A,C], E:[A,B,C]}
{A:[B,E], B:[E], C:[D,E], D:[A], E:[]}
{A:[(B,1), (E,2)], B:[(E,2)], C:[(D,1), (E,3)], D:[(A,3)], E:[]}
La matrice d'adjacence est un tableau carré (à double
entrée). Les lignes et les colonnes correspondent aux sommets (ordonnés
dans l'ordre alphanumérique si rien n'est précisé).
Si le sommet X et adjacent à Y (X-->Y), on remplit dans la matrice, à
l'intersection de la ligne de X et de la colonne de Y, un 1 (ou un V pour
«vrai»), ou bien une donnée si le graphe est valué (il y aura alors une
matrice d'adjacence selon chaque type de donnée). S'il n'y a pas de
relation entre X et Y, on note un 0 (ou un F pour «faux»).
Lorsque le graphe n'est pas orienté, la matrice d'adjacence est symétrique
par rapport à sa diagonale (diagonale principale : de en haut à gauche à
en bas à droite) ; aussi, les boucles apparaissent sur la diagonale.
[ [0, 1, 0, 1, 1], [1, 0, 0, 0, 1], [0, 0, 0, 1, 1], [1, 0, 1, 0, 0], [1, 1, 1, 0, 0] ]
[ [0, 1, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 1], [1, 0, 0, 0, 0], [0, 0, 0, 0, 0] ]
[ [0, 1, 0, 0, 2], [0, 0, 0, 0, 2], [0, 0, 0, 1, 3], [3, 0, 0, 0, 0], [0, 0, 0, 0, 0] ]
[ [0, 1, 0, 1, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [1, 0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0, 1] ]
{A:[B,D], B:[C], C:[D], D:[A,D], E:[B,F], F:[G], G:[E,G]}
{A:[D], B:[A,E], C:[B], D:[A,C,D], E:[G], F:[E], G:[F,G]}
Graphe |
---|
creer_graphe(ori) : vide |
est_oriente(G) : booléen |
ajouter_sommet(G, s) [sommet pas encore dans G] : vide |
sommet_existe(G, s) : booléen |
ajouter_arc(G, sd, sa, w) [sa et sd sont dans G, l'arc pas encore] : vide |
arc_existe(G, sd, sa) : booléen |
supprimer_sommet(G, s) [s est dans G] : vide |
supprimer_arc(G, sd, sa) [sa et sd sont dans G] : vide |
L'algorithme de parcours en largeur (ou BFS, pour Breadth First Search en anglais) permet le parcours d'un graphe ou d'un arbre de la manière suivante : on commence par explorer un nœud source, puis ses successeurs, puis les successeurs non-explorés des successeurs, etc. L'algorithme de parcours en largeur permet de calculer les distances de tous les nœuds depuis un nœud source dans un graphe non-pondéré (orienté ou non-orienté). Il peut aussi servir à déterminer si un graphe non-orienté est connexe.
Pseudo code : L'algorithme s'implémente à l'aide d'une file.
ParcoursLargeur(Graphe G, Sommet s) f = CreerFile() f.enfiler(s) marquer(s) tant que la file est non-vide s = f.defiler() afficher(s) pour tout voisin t de s dans G si t non-marqué f.enfiler(t) marquer(t)
Complexité : La complexité en temps dans le pire cas est en `O( | S | + | A | )` où `| S |` est le nombre de sommets et `| A |` est le nombre d'arcs. En effet, chaque arc et chaque sommet est visité au plus une seule fois.
L'exploration d'un parcours en profondeur depuis un sommet S fonctionne
comme suit : on suit un chemin dans le graphe jusqu'à un cul-de-sac ou
alors jusqu'à atteindre un sommet déjà visité. On revient alors sur le
dernier sommet où on pouvait suivre un autre chemin puis on explore un
autre chemin. L'exploration s'arrête quand tous les sommets depuis S ont
été visités. Bref, l'exploration progresse à partir d'un sommet S en
s'appelant récursivement pour chaque sommet voisin de S.
Le nom d'algorithme en profondeur est dû au fait que, contrairement à
l'algorithme de parcours en largeur, il explore en fait « à fond » les
chemins un par un : pour chaque sommet, il marque le sommet actuel, et il
prend le premier sommet voisin jusqu'à ce qu'un sommet n'ait plus de
voisins (ou que tous ses voisins soient marqués), et revient alors au
sommet père.
Si G n'était pas un arbre, l'algorithme pourrait a priori tourner
indéfiniment si on continuait l'exploration depuis un sommet déjà visité.
Pour éviter cela, on marque les sommets que l'on visite, de façon à ne pas
les explorer à nouveau. Dans le cas d'un arbre, le parcours en profondeur
est utilisé pour caractériser l'arbre.
explorer(graphe G, sommet s) marquer le sommet s afficher(s) pour tout sommet t voisin du sommet s si t n'est pas marqué alors explorer(G, t); parcoursProfondeur(graphe G, sommet s) pour tout sommet du graphe G en partant du sommet s si s n'est pas marqué alors explorer(G, s)
Ces algorithmes sont utilisés pour traiter des problèmes d'optimisation (transports, câblage électrique ou réseau, déplacements de pnj dans un jeu vidéo, ...).
Illustration de ces algorithmes sur une carte réelle avec : Pathfinding.
Le principe doit être vu à titre culturel et peut être travaillé comme approfondissement.
On pourra à ce titre consulter l'article : A*.