Favicon
NSI Terminale

Connexion élèves

Choisir le(s) module(s) à installer :

Algorithmes sur les graphes

Principe

Utilité

Comme on l'a déjà fait avec les arbres, on a souvent besoin de parcourir un graphe : 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.

Lors du parcours, les sommets peuvent être alors dans deux "états" :

Comme pour les arbres, 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.

Différence avec le parcours d'un arbre

Vous vous souvenez de l'algorithme de parcours d'un arbre, en profondeur par exemple :


traiter le nœud courant ( pour un parcours préfixe...)

si le nœud courant a un fils gauche:
    parcourir le fils gauche
si le nœud courant a un fils droit:
    parcourir le fils droit
			

On "descend" donc naturellement dans l'arbre jusqu'aux feuilles, il n'y a aucun moyen de "revenir sur ses pas" dans un arbre ( Rappel : un arbre est un graphe orienté acyclique ).

A l'inverse, dans un graphe, a fortiori dans un graphe non-orienté, il existe toujours une probabilité pour que l'on revienne sur un sommet déjà parcouru, si il existe l'arc/l'arête qui permette ce "retour en arrière"; le parcours "tourne alors en boucle" à l'infini...

Conséquence : dans le cas d'un graphe, pour éviter de parcourir plusieurs fois le même sommet, on va garder la trace des sommets déja visités :


traiter le sommet courant

si le sommet voisin 1 n'a pas encore été visité:
    marquer le voisin 1 comme visité 
    parcourir le graphe à partir de 1
si le sommet voisin 2 n'a pas encore été visité:
    marquer le voisin 2 comme visité
    parcourir le graphe à partir de 2
...
etc.( pour chaque sommet voisin du sommet courant)
			

Pour garder la trace des sommets déjà visités, il y a plusieurs solutions :

Parcours en profondeur d'abord

Algorithme

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.

Parcours récursif


fonction parcours_profondeur_récursif: G = graphe, u = sommet ( celui de départ au premier appel )

	traiter le sommet u

	pour chaque sommet v voisins sommet u:
		si v n'a pas encore été visité :
		    marquer v comme visité
			 parcours_profondeur_récursif(G, v)
			

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 ( voir l'implémentation au chapitre suivant ).

Parcours itératif

Pour déterminer l'ordre de parcours des sommets, c'est donc ici le dernier visité qui sera le premier exploré : on utilise donc une structure LIFO ( une pile ) pour stocker les sommets à explorer :


fonction parcours_profondeur: G = graphe, s = sommet de départ

	pile ← pile vide
	 
	marquer s comme visité
	enpiler(s, pile)
	
	tant que pile non vide:
		u = dépiler(pile)
		traiter le sommet u
		pour chaque sommet v adjacent au sommet u :
			si v n'a pas encore été visité:
				marquer v comme visité
				empiler(v, pile)	
			

Travail sur un exemple


Pile ⇄ Parcours

( On remarque que l'algorithme semble "revenir en arrière" à chaque intersection : on parle de backtracking, et cela a de nombreuses applications...)

Parcours en largeur d'abord

Algorithme

Pour déterminer l'ordre de parcours des sommets, c'est le premier visité qui sera le premier exploré : 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 = départ du parcours

	file ← file vide
		 	
	enfiler(s,file)
	marquer s comme visité
	
	tant que file non vide:
		u = défiler(file)
		traiter le sommet u
		pour chaque sommet v adjacent au sommet u :
			si v n'a pas encore été visité:
				enfiler(v, file)	
				marquer v comme visité
			

( oh mais dites donc, c'est exactement le même que le parcours en profondeur itératif, avec une file à la place d'une pile 😮 ...)

Travail sur un exemple


← 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.

A retenir :

  • parcours en Profondeur → utilisation d'une Pile
  • parcours en Largeur → utilisation d'une file

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).

Exercices d'entraînement

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 )