Aller au contenu principal

Parcourir un graphe

Ce que ce chapitre apporte 8 points
  • Dérouler à la main un parcours en largeur et un parcours en profondeur.
  • Expliquer pourquoi la file donne les couches et la pile donne les branches.
  • Écrire les deux parcours, en itératif, et savoir où marquer un sommet dans chacun.
  • Obtenir l'ordre d'arrivée et l'ordre de fin d'un parcours en profondeur.
  • Calculer les distances et les plus courts chemins en nombre d'arêtes.
  • Décomposer un graphe en composantes connexes.
  • Détecter un cycle, et tester si un graphe est biparti.
  • Justifier la complexité Θ(n + m).
Presque tout ce qu'on demande à un graphe commence par le parcourir : atteindre tous les sommets, sans en oublier, sans tourner en rond. Il y a deux façons de le faire : elles diffèrent par la réserve où l'on range les sommets en attente, file ou pile, et ce rangement change tout ce qu'on obtient au bout. Ce chapitre déroule les deux pas à pas sur le même graphe, puis montre ce que chacune permet de calculer, du plus court chemin à la détection de cycle. Le parcours Mathématiques appliquées les revoit plus brièvement, au sein d'un chapitre qui va du vocabulaire à l'ordonnancement en une seule traversée.

Une seule idée, deux rangements

Le principe commun tient en trois phrases. On garde une réserve de sommets découverts mais pas encore traités. Tant qu'elle n'est pas vide, on en sort un, on le traite, et on y met ses voisins pas encore visités. Une marque empêche de traiter deux fois le même sommet ; le moment où l'on pose cette marque dépend de la réserve, et la suite du chapitre dit pourquoi.

Deux réserves

Le parcours en largeur (BFS) sort de la réserve le sommet le plus anciennement entré : la réserve est une file.

Le parcours en profondeur (DFS) sort le sommet le plus récemment entré : la réserve est une pile.

La file et la pile sont les deux structures du chapitre d'algorithmique sur les piles et les files, qui déroule déjà un parcours en largeur sur les postes d'un atelier.

Pourquoi cela change tout. Une file traite les sommets par ordre de découverte, donc par distance croissante au départ : le parcours avance par couches concentriques.
Une pile traite en priorité le dernier découvert, donc s'enfonce le long d'une branche jusqu'au bout avant de revenir : le parcours suit la structure du graphe.
La file donne les distances. La pile donne les branches, et avec elles les hiérarchies et l'ordre dans lequel on termine les sommets.

Le parcours en largeur, pas à pas

Dérouler la figure ci-dessous pas à pas. La file y est dessinée sous le graphe, comme une rangée de jetons : un sommet entre à droite et sort à gauche. Le petit numéro posé sur un sommet est son rang de visite, le nombre écrit dessous sa distance au départ, et les arêtes en gras forment l'arbre de parcours : celles par lesquelles chaque sommet a été découvert. Quand un sommet a plusieurs voisins nouveaux, ils sont pris dans l'ordre de sa liste de voisins, ici l'ordre alphabétique. C'est une convention : une autre donnerait un autre ordre de visite, et la figure écrit sous le graphe les listes qu'elle suit.

Parcours avec une file, depuis A7 sommets, 8 arêtes
0 / 7
Réserve
Marque
Voisins
Cliquer un sommet pour en faire le départ.
Adist 0BCDEFG
Adist 0BCDEFG

Sommets rangés par distance au départ. Plein : traité, avec son rang. Contour tireté : marqué, en attente dans la réserve. En gras : l'arête par laquelle chaque sommet a été découvert.

Voisins, dans l'ordre où le parcours les prend (alphabétique) :

  • A : B, C
  • B : A, D
  • C : A, D, E
  • D : B, C, F
  • E : C, F
  • F : D, E, G
  • G : F
FileEntrées dans la file : 1 pour 7 sommets
sort ←
  1. Adépart
← entre
Ordre de visite : aucun sommet encore
Départ : A entre dans la file et est marqué aussitôt. Rien n'est encore traité.
Parcours en largeur depuis A, voisins pris dans l'ordre alphabétique. Chaque colonne rassemble les sommets à même distance de A, et les arêtes en gras forment l'arbre de parcours.
À manipuler
Dérouler le parcours en suivant la file : chaque sommet y entre une seule fois, sept entrées pour sept sommets, et les rangs balaient les colonnes de gauche à droite, c'est-à-dire les distances. Basculer ensuite la marque « à la sortie » : l'ordre de visite ne change pas, mais D et F entrent deux fois dans la file, neuf entrées pour sept sommets, et chaque copie en trop ressort pour rien. Revenir à la marque à l'entrée, puis passer les voisins en « ordre d'écriture », où C précède B dans la liste de A : la visite devient A C B E D F G, et chaque sommet reste pourtant dans sa colonne. Cliquer enfin un autre sommet pour en faire le départ : les colonnes se refont autour de lui.

L'ordre de visite est A B C D E F G. Ce qui n'est pas arbitraire, c'est la succession des couches : d'abord le sommet de départ, puis tous ceux à une arête (B, C), puis tous ceux à deux arêtes (D, E), et ainsi de suite ; sur la figure, ce sont les colonnes. À l'intérieur d'une couche, l'ordre vient de la convention alphabétique.

La propriété qui fait tout l'intérêt du BFS

Le BFS visite les sommets par distance croissante au sommet de départ. La distance dist(v)\mathrm{dist}(v) qu'il calcule est donc la longueur du plus court chemin du départ à vv, comptée en nombre d'arêtes.

Dans un BFS, marquer à l'enfilement, jamais au défilement
Dans un BFS, un sommet doit être marqué « vu » au moment où on l'ajoute à la file.
Si l'on attend de le retirer, deux voisins peuvent l'ajouter chacun à leur tour avant qu'il ne sorte, et il entre deux fois dans la file. Sur un simple triangle parcouru depuis A, C est ajouté par A puis par B, et s'il reçoit sa distance à chaque ajout, il finit à distance 2 au lieu de 1. Sur un graphe plus grand, la file enfle, et si chaque copie est retraitée comme un sommet neuf, une grille de 12 cases sur 12 fait entrer 2,7 millions de sommets dans la file au lieu de 144.
C'est l'erreur la plus fréquente sur ce code. Elle ne se voit pas sur un chemin, où chaque sommet n'a qu'un voisin déjà découvert, et se voit dès qu'un sommet en a deux.
Vérification rapideon peut se reprendre

1.Lequel des deux parcours donne les plus courts chemins en nombre d'arêtes ?

2.À quel moment marque-t-on un sommet comme vu, dans un BFS ?

Le parcours en profondeur, sur le même graphe

Même graphe, même départ, une pile au lieu d'une file. Regarder comme le tracé change de nature, et suivre la pile sous le graphe : un sommet y entre et en sort par le dessus, et un même sommet peut y entrer plusieurs fois. Les sommets restent rangés par distance à A, ce qui fait voir le parcours aller et venir entre les colonnes au lieu de les balayer dans l'ordre.

Parcours avec une pile, depuis A7 sommets, 8 arêtes
0 / 9
Réserve
Marque
Voisins
Cliquer un sommet pour en faire le départ.
ABCDEFG
ABCDEFG

Sommets rangés par distance au départ. Plein : traité, avec son rang. En gras : l'arête par laquelle chaque sommet a été découvert.

Voisins, dans l'ordre où le parcours les prend (alphabétique) :

  • A : B, C
  • B : A, D
  • C : A, D, E
  • D : B, C, F
  • E : C, F
  • F : D, E, G
  • G : F
PileEntrées dans la pile : 1 pour 7 sommets
fond
  1. Adépart
dessus : entre et sort
Ordre de visite : aucun sommet encore
Départ : A entre dans la pile. Rien n'est encore traité.
Parcours en profondeur depuis A, voisins pris dans l'ordre alphabétique. Un sommet peut entrer plusieurs fois dans la pile : il n'est marqué qu'à sa sortie.
À manipuler
Dérouler jusqu'au bout : la pile reçoit neuf entrées pour sept sommets, C et F deux fois chacun, et l'arbre en gras descend jusqu'à la profondeur 6, quand celui du parcours en largeur s'arrêtait à 4. Basculer alors la marque « à l'entrée », comme dans le parcours en largeur : l'ordre devient A B D F E G C, et la figure désigne le premier écart avec un vrai parcours en profondeur, au quatrième rang. Basculer enfin la réserve sur « file » : le même code redonne le parcours en largeur.

L'ordre est A B D C E F G. Le BFS avait visité C en troisième position, le DFS le visite en quatrième, après être descendu jusqu'à D. Et surtout, l'arbre de parcours du DFS est un long chemin, A B D C E F G, là où celui du BFS était étalé.

Dans la pile, on marque à la sortie
La règle « marquer à l'enfilement » vaut pour la file, pas pour la pile. Un DFS itératif qui marquerait ses sommets en les empilant ne serait plus un parcours en profondeur : sur le graphe ci-dessus, il visiterait A B D F E G C, et quitterait E pour G alors que C, voisin de E, n'est pas encore visité. Marqué dès le départ par A, C ne peut plus être atteint depuis D, et le parcours cesse de s'enfoncer.
Le DFS itératif marque donc un sommet quand il sort de la pile, et ignore une copie déjà visitée qui ressort. Un même sommet peut ainsi entrer plusieurs fois dans la pile, C et F deux fois chacun ici : c'est le prix d'un vrai ordre en profondeur, et il reste borné par le nombre d'arêtes.

Les deux parcours en Python

Le code emploie une file toute faite : deque, du module collections, où append ajoute à droite et popleft() retire à gauche l'élément le plus ancien, en temps constant, là où liste.pop(0) décalerait tous les autres. La pile, elle, est une simple liste : append empile, pop() dépile le dernier arrivé. Le graphe est un dictionnaire qui associe à chaque sommet la liste de ses voisins, rangés ici par ordre alphabétique.

main.py
Sortie
>_ Prêt à exécuter…

Les voisins sont rangés dans des listes, qui gardent leur ordre : le résultat est le même à chaque exécution. Avec des ensembles set, qui n'ont pas d'ordre fixe, le même BFS pourrait rendre A B C D E F G une fois et A C B E D F G la suivante, et il faudrait trier les voisins avant de les parcourir.

La distance n'est pas le chemin
Le BFS calcule les distances, mais pour afficher le trajet il faut mémoriser, pour chaque sommet, celui qui l'a découvert. C'est le tableau parent, qui ne coûte qu'une case par sommet.
On remonte ensuite de la cible vers le départ, puis on retourne la liste. Ce mécanisme resservira à l'identique pour Dijkstra.

La version récursive, et l'ordre de fin

Récursif ou itératif
Le DFS s'écrit naturellement en récursif : la pile d'appels de la machine tient lieu de pile explicite, et le code tient en quelques lignes. Le chapitre sur la récursivité le dit : pour une exploration, la version récursive est la plus simple à écrire.
Ce qui la condamne ici n'est pas sa lisibilité mais la profondeur. En Python, la limite par défaut est d'environ mille appels imbriqués : un chemin de dix mille sommets fait échouer le programme. La version itérative avec une pile explicite n'a pas cette limite.

Écrit en récursif, le parcours en profondeur donne deux ordres d'un coup : celui dans lequel on arrive sur les sommets, et celui dans lequel on les termine, quand tout ce qui a été découvert depuis eux est exploré.

main.py
Sortie
>_ Prêt à exécuter…

La version récursive marque un sommet en y arrivant, et c'est juste : l'appel pour C n'a lieu qu'au moment où l'on s'y enfonce, pas quand A le découvre. L'ordre d'arrivée, dit aussi ordre préfixe, est celui de la figure : A B D C E F G.

L'ordre de fin

L'ordre de fin, dit aussi ordre postfixe, range chaque sommet après tous ceux qui ont été découverts depuis lui, ses descendants dans l'arbre de parcours. Ici G F E C D B A : D ne se termine qu'une fois C, E, F et G explorés, et le départ A se termine en dernier.

C'est l'ordre qu'il faut chaque fois qu'un sommet ne peut être traité qu'après tout ce qui en dépend. Dans un arbre, il traite les enfants d'un nœud avant le nœud lui-même : c'est ainsi qu'on résout un arbre de jeu en partant des feuilles, dans le chapitre sur les jeux séquentiels. Sur un graphe orienté sans circuit, en relançant le parcours depuis chaque sommet non vu, l'ordre de fin lu à l'envers donne un tri topologique, question que le chapitre sur les graphes orientés traite par une autre méthode.

Ce que la complexité vaut

Dans un BFS, chaque sommet entre au plus une fois dans la file. Dans le DFS itératif, un sommet peut entrer dans la pile une fois par arête qui le touche (neuf empilements pour sept sommets sur le graphe du chapitre, comme le compte la figure), soit au plus 2m+12m + 1 entrées en tout. Dans les deux cas, chaque sommet n'est traité qu'une fois, et chaque arête est examinée deux fois, une par extrémité. D'où, avec des listes d'adjacence :

BFS et DFS:O(n+m) depuis un sommet,Θ(n+m) pour le parcours complet\text{BFS et DFS} : O(n + m) \text{ depuis un sommet}, \quad \Theta(n + m) \text{ pour le parcours complet}

Le parcours depuis un sommet s'arrête à sa composante, et peut donc coûter bien moins ; le parcours complet, relancé depuis chaque sommet non vu, atteint tout le graphe et coûte exactement de l'ordre de n+mn + m.

Avec une matrice, trouver les voisins d'un sommet coûte Θ(n)\Theta(n) au lieu de Θ(deg⁡(v))\Theta(\deg(v)), et le parcours complet devient Θ(n2)\Theta(n^2). Sur les 200 000 carrefours du chapitre précédent, à dix millions de cases lues par seconde, c'est la différence entre moins d'une seconde et plus d'une heure.

Vérification rapideon peut se reprendre

Combien coûte un parcours complet sur un graphe donné par sa matrice d'adjacence ?

Ce qu'on obtient en plus, presque gratuitement

Les composantes connexes

Un parcours atteint exactement la composante de son sommet de départ. Pour décomposer tout le graphe, on relance un parcours depuis chaque sommet pas encore vu.

Graphe non orienté8 sommets, 6 arêtes
ABCDEFGH
Trois composantes connexes, une couleur chacune : un parcours lancé depuis A n'atteint que A, B et C.

Trois couleurs, trois composantes. Le coût total reste Θ(n+m)\Theta(n + m), puisque chaque sommet n'est traité que dans un seul parcours.

Une vanne fermée, des quartiers privés d'eau
Un réseau de distribution d'eau se modélise en graphe : les sommets sont les nœuds du réseau (château d'eau, jonctions de canalisations, quartiers), les arêtes les tronçons de canalisation. Fermer une vanne, c'est retirer une arête.
La question « quels quartiers restent alimentés ? » devient : quels sommets un parcours lancé depuis le château d'eau atteint-il encore ? Les autres forment une ou plusieurs composantes à part, et ce sont elles qu'il faut desservir autrement. La même question se pose pour un réseau électrique après une panne, ou un réseau informatique après la coupure d'un câble.

Le test de bipartisme

Un graphe est biparti si et seulement s'il n'a aucun cycle impair. Le BFS le teste en une passe : on colorie le sommet de départ en 0, chaque voisin découvert reçoit la couleur opposée à celle du sommet qui le découvre, et l'on vérifie qu'aucune arête ne relie deux sommets de même couleur.

Graphe non orienté5 sommets, 6 arêtes
ABCDE
Le graphe est biparti : un BFS parti de A donne la couleur 0 à A et C, la couleur 1 à B, D et E, et aucune arête ne relie deux sommets de même couleur.

Quand une arête relie deux sommets de même couleur, le test échoue. Sur le graphe suivant, le BFS parti de A donne la couleur 0 à D et à E, tous deux à distance 2, et l'arête D-E les relie. Cette arête ne suffit pas à convaincre : elle ne dit pas pourquoi ses deux extrémités devaient recevoir la même couleur. Ce qui le dit, c'est le cycle qu'elle ferme avec les deux branches de l'arbre de parcours qui mènent à ses extrémités, remontées jusqu'à leur premier ancêtre commun, ici A.

Graphe non orienté6 sommets, 6 arêteschemin mis en évidence
ABCDEF
L'arête D-E relie deux sommets de couleur 0. Avec les branches D, B, A et A, C, E de l'arbre, elle ferme le cycle D, B, A, C, E, D, de longueur 5 : ce cycle impair prouve que le graphe n'est pas biparti.

La longueur de ce cycle est toujours impaire. Les deux branches descendent du même ancêtre, et leurs extrémités sont à la même distance du départ ou à des distances qui diffèrent d'une unité ; comme elles portent la même couleur, elles sont à la même distance, donc les deux branches ont la même longueur kk, et le cycle a 2k+12k + 1 arêtes.

La détection de cycle

Dans un graphe non orienté, le DFS trouve un cycle dès qu'il rencontre une arête vers un sommet déjà vu qui n'est pas son père immédiat. Cette arête, dite arête arrière, ferme un cycle avec le chemin de l'arbre. Dans un graphe non orienté, un BFS le ferait aussi bien, avec le même critère ; c'est dans un graphe orienté que le DFS devient indispensable, comme le montrera le chapitre sur les graphes orientés.

main.py
Sortie
>_ Prêt à exécuter…

Lequel pour quoi.

BFS : plus court chemin en nombre d'arêtes, distance, couches, bipartisme. Il donne les distances parce qu'il explore par couches.
DFS : structure arborescente, ordre de fin, et, dans un graphe orienté, détection des circuits et tri topologique. Il révèle la structure parce qu'il suit les branches jusqu'au bout.
Quand les arêtes portent des poids positifs, la file devient une file de priorité, qui rend toujours le sommet en attente le plus proche du départ, et le BFS devient Dijkstra ; avec des poids négatifs, il faut autre chose. C'est le chapitre 5.

Mettre en pratique

Pourquoi le BFS donne-t-il les plus courts chemins, et pas le DFS ?

Parce que la file traite les sommets dans l'ordre où ils ont été découverts, donc par distance croissante au départ.

Quand le BFS retire un sommet à distance kk, tous les sommets à distance kk ou moins sont déjà découverts. Le premier chemin trouvé vers un sommet est donc le plus court.

Le DFS, lui, s'enfonce dans une branche : sur le graphe de ce chapitre, il atteint C par A, B, D, C, trois arêtes pour un voisin direct du départ. Le chemin qu'il trouve est un chemin, jamais garanti minimal.

Que se passe-t-il si, dans un BFS, on marque un sommet au défilement plutôt qu'à l'enfilement ?

Le même sommet peut entrer plusieurs fois dans la file, une fois par voisin qui le découvre avant qu'il n'en sorte.

Conséquences : si chaque copie est retraitée comme un sommet neuf, le nombre d'entrées explose, jusqu'à 2,7 millions pour les 144 cases d'une grille de 12 sur 12 ; si l'on écrit la distance à chaque ajout, une valeur plus grande peut écraser la bonne, dès un triangle. Ignorer les copies à leur sortie évite de les retraiter, mais pas d'écraser la distance : la seule règle sûre, dans un BFS, est de marquer à l'entrée dans la file.

Le parcours reste juste sur un chemin, où chaque sommet n'a qu'un voisin déjà découvert : c'est pour cela qu'un exemple mal choisi ne montre rien.

Comment tester qu'un graphe est biparti, et que fournir comme preuve du contraire ?

Par un BFS bicolore : on colorie le départ en 0, chaque sommet découvert reçoit la couleur opposée à celle de son découvreur.

Si l'on rencontre une arête entre deux sommets de même couleur, le graphe n'est pas biparti : cette arête et les deux branches de l'arbre de parcours qui mènent à ses extrémités, remontées jusqu'à leur premier ancêtre commun, forment un cycle de longueur impaire.

C'est ce cycle qu'il faut donner comme preuve : chacun peut vérifier ses arêtes et compter sa longueur. L'arête seule ne prouve rien, car elle ne dit pas pourquoi ses deux extrémités devaient recevoir la même couleur ; et affirmer « j'ai essayé toutes les partitions » ne prouve rien non plus.

Un DFS récursif échoue avec « maximum recursion depth exceeded ». Pourquoi, et que faire ?

Parce que la profondeur de récursion suit la longueur de la plus longue branche de l'arbre de parcours. Sur un chemin de 10 000 sommets, cela fait 10 000 appels imbriqués, alors que Python en autorise environ mille par défaut.

Deux réponses. Augmenter la limite avec sys.setrecursionlimit règle le symptôme, mais la pile système peut déborder pour de bon.

La bonne réponse est la version itérative avec une pile explicite : elle n'a d'autre limite que la mémoire disponible. Il faut alors marquer les sommets à leur sortie de la pile, et non à leur entrée.

Comment compter les composantes connexes, et quel est le coût total ?

On parcourt les sommets ; chaque fois qu'on en trouve un non visité, on lance un parcours complet depuis lui et on compte une composante de plus.

Le coût total reste Θ(n+m)\Theta(n + m), et non Θ(n×(n+m))\Theta(n \times (n+m)) : chaque sommet et chaque arête n'est traité que dans un seul parcours, celui de sa propre composante.

C'est le même schéma pour toute propriété qui se calcule composante par composante.

Sur un graphe donné par sa matrice d'adjacence, quelle est la complexité d'un BFS complet ?

Θ(n2)\Theta(n^2).

Chaque sommet est défilé une fois, et pour chacun il faut lire toute sa ligne de matrice pour trouver ses voisins, soit nn cases, y compris les zéros.

Sur un graphe creux où m≈nm \approx n, cela transforme un algorithme linéaire en algorithme quadratique. C'est la raison la plus concrète de préférer les listes d'adjacence.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices