Un graphe a pour "vocation" principale à être parcouru : c'est en effet ainsi que l'on va pouvoir par exemple trouver un chemin d'un sommet à un autre, trouver le plus court chemin entre deux sommets, vérifier qu'il existe un chemin entre deux sommets, etc...
Les applications en sont très nombreuses : trouver le chemin de sortie d'un labyrinthe, déterminer le plus court chemin entre deux routeurs dans un réseau ou entre deux villes sur une carte, déterminer le score PageRank © donné par Google aux pages web qu'il recense, etc...
Parcourir un graphe, c'est "l'explorer" sommet par sommet en partant d'un sommet de départ, le but étant de "découvrir" tous les sommets du graphe en fonction des relations qui existent entre eux.
Deux manières d'explorer un graphe existent :
On a donc besoin de connaître les voisins de chaque sommet : l'implémentation sous forme de listes d'adjacence semble donc bien adaptée.
On part d'un sommet initial du graphe appelé la racine de l'exploration.
Les sommets peuvent être dans deux "états" :
L'idée ici est qu'un sommet aura été "entièrement exploré" quand tous ses voisins directs ont été "découverts", puis on passe à l'exploration du sommet suivant.
Pour déterminer l'ordre de parcours des sommets, c'est le premier marqué qui sera le premier traité : on utilise donc une structure FIFO ( une file ) pour stocker les sommets à explorer une fois qu'ils ont été découverts.
fonction parcours_largeur: G = graphe, s = racine
file ← file vide
marque ← dictionnaire associant à chaque sommet un booléen, initialisé à Faux ( Faux = "non découvert" )
parcours ← liste vide qui stockera les sommets successivement parcourus
marque[s] = Vrai
enfiler(s,file)
tant que file non vide:
u = défiler(file)
pour chaque sommet v adjacent au sommet u :
si marque[v] est Faux alors
marque[v] = Vrai
enfiler(v, file)
stocker u dans parcours
renvoyer parcours
← File ← | Parcours |
---|---|
Aucune "branche" du graphe n'est plus importante qu'une autre ( il n'y a pas d'ordre sur les sommets ) : il y a donc plusieurs possibilités équivalentes de parcours selon la "branche" que l'on choisit de parcourir en premier, lorsque ce choix se présente.
On part là aussi d'un sommet initial du graphe appelé la racine de l'exploration, et on garde la trace des sommets déjà visités.
Un sommet a été totalement exploré quand tous ses voisins, puis les voisins de ses voisins, etc... ont été "découverts", puis on passe à l'exploration du sommet suivant.
Pour déterminer l'ordre de parcours des sommets, c'est donc ici le dernier marqué qui sera le premier traité : on utilise donc une structure LIFO ( une pile ) pour stocker les sommets à explorer.
fonction parcours_profondeur: G = graphe, s = racine
pile ← pile vide
marque ← dictionnaire associant à chaque sommet un booléen, initialisé à Faux ( Faux = "non découvert" )
parcours ← liste vide qui stockera les sommets successivement parcourus
marque[s] = Vrai
enpiler(s,pile)
tant que pile non vide:
u = dépiler(pile)
pour chaque sommet v adjacent au sommet u :
si marque[v] est Faux alors
marque[v] = Vrai
empiler(v, pile)
stocker u dans parcours
renvoyer parcours
Pile ⇄ | Parcours |
---|---|
Vous le savez, dès qu'on utilise la récursivité, on met en jeu une pile qui stocke les résultats des appels récursifs de fonctions; ici, l'empilage et le dépilage des sommets à explorer peuvent être remplacés par des appels récursifs à la fonction de parcours :
fonction parcours_profondeur_récursif: G = graphe, u = sommet, marque, parcours
marque[u] = Vrai
stocker u dans parcours
pour chaque sommet v adjacent au sommet u:
si marque[v] est faux alors :
parcours_profondeur_récursif(G, v, marque, parcours)
renvoyer parcours
Simple, non ? Et si, il y a bien un cas de base qui fait que la récursion se termine : quand il n'y a plus de sommets non marqués, il n'y a plus d'appels récursif !
L'inconvénient est de passer à chaque récursion de nombreux arguments...
On peut remarquer que :
Cela n'a dans la pratique aucune importante, il n'y a pas d'ordre particulier sur les sommets !
A retenir :
Les deux algorithmes de parcours nécessitent de parcourir chacun des sommets et chacune des arètes/des arcs du graphe une et une seule fois : la complexité en temps de ces algorithmes est donc directement proportionnelle à la somme des nombres de sommets ( N ) et d'arcs/d'arètes ( M ) du graphe, soit en O(N + M).
Entraînez-vous à l'aide de l'animation ci-dessous sur quelques exemples de parcours en largeur et en profondeur.
N'oubliez pas qu'à "niveau" de parcours équivalent, il y a ( parfois ) plusieurs possibilités pour un même type de parcours selon le choix de la branche que l'on décide de parcourir d'abord.
BFS ( largeur d'abord ) |
---|
DFS ( profondeur d'abord ) |
---|