edubun
Français

Parcours de graphes : BFS et DFS

En 1736, Leonhard Euler a montré qu'aucune promenade ne traverse chacun des sept ponts de Königsberg exactement une fois, et la théorie des graphes est née de cette réponse.

Des points et des traits

Un graphe est un ensemble de points, appelés sommets ou nœuds, reliés par des traits, appelés arêtes. L'idée est simple, et elle s'applique à une foule de choses : des villes reliées par des routes, des pages web reliées par des liens, des personnes reliées par des amitiés, les cases d'un labyrinthe reliées là où il n'y a pas de mur. Une fois qu'un problème est dessiné sous forme de graphe, la même poignée d'algorithmes peut répondre à des questions comme « puis-je aller d'ici à là ? » et « quel est le chemin le plus court ? »

ContexteLa ville de Königsberg (aujourd'hui Kaliningrad) avait sept ponts reliant deux îles et les deux rives de sa rivière. On se demandait si une promenade pouvait traverser chaque pont exactement une fois. En 1736, Leonhard Euler a répondu que non. Son idée clé a été de jeter la carte : seules comptaient les zones de terre et les ponts qui les reliaient. Chaque zone de terre est devenue un point et chaque pont un trait. Il a ensuite montré qu'une telle promenade exige que chaque point, sauf peut-être le départ et l'arrivée, ait un nombre pair de traits, alors qu'à Königsberg les quatre zones de terre en avaient un nombre impair.
A B C D E F
Le graphe d'exemple de cette unité. A est le départ ; F est l'arrivée. Arêtes : A‑B, A‑C, B‑D, C‑D, C‑E, D‑F, E‑F.

Stocker un graphe dans du code

Une façon courante de stocker un graphe est la liste d'adjacence : pour chaque sommet, la liste de ses voisins. Les notes du cours 6.006 du MIT utilisent exactement cela, un tableau ou dictionnaire Adj où Adj[u] liste les sommets reliés à u. Pour le graphe ci-dessus, cela donne :

const graph = {   // liste d'adjacence
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "D", "E"],
  D: ["B", "C", "F"],
  E: ["C", "F"],
  F: ["D", "E"],
};

Le parcours en largeur : anneau par anneau

Le parcours en largeur (BFS, breadth-first search) explore un graphe par anneaux autour du départ. D'abord le départ lui-même, à distance 0. Puis tous ses voisins, à distance 1. Puis chaque nouveau sommet voisin de ceux-là, à distance 2, et ainsi de suite. Pour cela, il utilise une file, une file d'attente où le premier entré est le premier sorti. On prend le sommet en tête de file, on le visite et on ajoute ses voisins pas encore vus en queue de file. À partir de A dans l'exemple, le BFS visite A, puis B et C, puis D et E, puis F. Comme il avance d'un anneau à la fois, la première fois que le BFS atteint un sommet, il a trouvé un chemin avec le moins d'arêtes possible. F est à 3 arêtes de A, par exemple A, B, D, F.

function bfs(graph, start) {
  const seen = new Set([start]);
  const queue = [start];
  const order = [];
  while (queue.length) {
    const u = queue.shift();        // premier entré, premier sorti
    order.push(u);
    for (const v of graph[u]) {
      if (!seen.has(v)) { seen.add(v); queue.push(v); }
    }
  }
  return order;   // A B C D E F
}

Le parcours en profondeur : aussi loin que possible, puis retour en arrière

Le parcours en profondeur (DFS, depth-first search) se comporte comme une personne qui garde une main sur le mur dans un labyrinthe. Il suit un chemin aussi loin que possible. Quand il arrive à une impasse ou à un endroit déjà vu, il revient au dernier embranchement et essaie l'option suivante. Il utilise une pile au lieu d'une file, dernier entré, premier sorti, et il s'écrit très naturellement avec la récursivité, où la pile d'appels se charge de se souvenir. À partir de A, un DFS récursif visite A, B, D, C, E, F. Le DFS atteint tous les sommets que le BFS atteint, mais le chemin qu'il suit n'est pas forcément le plus court.

function dfs(graph, u, seen = new Set(), order = []) {
  seen.add(u);
  order.push(u);
  for (const v of graph[u]) if (!seen.has(v)) dfs(graph, v, seen, order);
  return order;   // A B D C E F (en profondeur)
}
Chargement

Fais glisser pour tourner. Fais défiler ou pince pour zoomer.

Un labyrinthe vu comme un graphe : chaque case libre est un sommet, relié aux cases libres voisines. Lance un BFS, puis un DFS sur le même labyrinthe, et compare les cases visitées et la longueur du chemin trouvé.

Lequel utiliser

Dans le premier labyrinthe du labo, le BFS visite 105 cases et trouve un chemin de 22 déplacements, le plus court possible. Le DFS plonge par chance vers l'arrivée et ne visite que 31 cases, mais son chemin demande 28 déplacements. Aucun n'est toujours moins coûteux. Les deux traitent chaque sommet et chaque arête un nombre fixe de fois : tous deux s'exécutent donc en temps O(V + E) pour V sommets et E arêtes. Choisis le BFS quand il te faut le moins d'étapes : le moins de coups dans un casse-tête, le moins de liens entre deux personnes. Choisis le DFS quand il faut tout explorer ou suivre des choix jusqu'au bout : vérifier que toutes les pièces d'un labyrinthe communiquent, ou essayer les coups d'un casse-tête une ligne à la fois en revenant en arrière quand on est bloqué. Quand les arêtes ont des longueurs différentes, comme les routes, aucun des deux ne suffit seul ; des méthodes comme l'algorithme de Dijkstra prolongent l'idée du BFS avec des distances.

Les deux parcours doivent se souvenir des sommets déjà vus. Sans cela, un graphe avec une boucle (de A à B à D à C et retour à A) ferait tourner le parcours en rond indéfiniment.
grapheDes sommets reliés par des arêtes
sommet (nœud)Un point d'un graphe
arêteUn trait qui relie deux sommets
liste d'adjacencePour chaque sommet, la liste de ses voisins
filePremier entré, premier sorti ; utilisée par le BFS
BFSParcours en largeur : anneau par anneau ; trouve les chemins avec le moins d'arêtes
DFSParcours en profondeur : d'abord en profondeur, puis retour en arrière ; utilise une pile
O(V + E)Le temps d'exécution du BFS et du DFS
  1. Dans le graphe d'exemple, combien d'arêtes compte le plus court chemin de A à F ?
  2. Combien d'arêtes compte le graphe d'exemple ?
  3. Quelle structure de données le parcours en largeur utilise-t-il ?
    • Une file (premier entré, premier sorti)
    • Une pile (dernier entré, premier sorti)
    • Une liste triée
    • Aucune structure
  4. Quel parcours est sûr de trouver un chemin avec le moins d'arêtes ?
    • BFS
    • DFS
    • Les deux, toujours
    • Aucun
  5. Range les sommets dans l'ordre où le BFS depuis A les visite (voisins pris par ordre alphabétique).
  6. Range les sommets dans l'ordre où le DFS récursif depuis A les visite (voisins par ordre alphabétique).
  7. Sur la grille de 15 sur 9 du labo, sans aucun mur, combien de déplacements compte le plus court chemin de S (en haut à gauche) à G (en bas à droite) ?
  8. Pourquoi le BFS et le DFS doivent-ils se souvenir des sommets déjà vus ?
    • Sinon, une boucle dans le graphe ferait tourner le parcours en rond indéfiniment
    • Pour les afficher à la fin
    • Pour les trier
    • Ce n'est nécessaire que pour le DFS
  9. En quelle année Euler a-t-il résolu le problème des ponts de Königsberg ?
    • 1683
    • 1736
    • 1843
    • 1945
  10. Quel est le temps d'exécution du BFS sur un graphe de V sommets et E arêtes ?
    • O(V + E)
    • O(V × E)
    • O(log V)
    • O(2ⱽ)
  11. Tu veux connaître le plus petit nombre de liens « ami d'un ami » entre deux personnes dans un réseau social. Quel parcours ?
    • BFS
    • DFS
    • Recherche dichotomique
    • Tri fusion
L'essentiel
  • Un graphe est un ensemble de sommets reliés par des arêtes ; les listes d'adjacence le stockent.
  • Le BFS utilise une file, avance anneau par anneau et trouve les chemins avec le moins d'arêtes.
  • Le DFS utilise une pile (ou la récursivité), va d'abord en profondeur, puis revient en arrière.
  • Les deux s'exécutent en O(V + E) et doivent se souvenir des sommets vus.
  • La solution d'Euler pour les ponts de Königsberg, en 1736, a fait naître la théorie des graphes.
Sources de ce cours (11)
  • MIT OpenCourseWare 6.006 Introduction to Algorithms, Spring 2020 (Demaine, Ku, Solomon)
  • lecture notes 1 Introduction, 3 Sorting, 9 Breadth-First Search, 10 Depth-First Search (ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/pages/lecture-notes) and Fall 2011 lecture 13 BFS notes (ocw.mit.edu/courses/6-006-introduction-to-algorithms-fall-2011)
  • Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (MIT Press)
  • with the CLRS-based Khan Academy algorithms unit by Thomas Cormen and Devin Balkcom of Dartmouth (khanacademy.org/computing/computer-science/algorithms: binary search, selection sort analysis, merge sort, quicksort overview and analysis)
  • MacTutor History of Mathematics, Al-Khwarizmi (mathshistory.st-andrews.ac.uk/Biographies/Al-Khwarizmi: word algorithm from the Latin title Algoritmi de numero Indorum)
  • D. E. Joyce, Euclid's Elements Book VII Propositions 1 and 2 (mathcs.clarku.edu/~djoyce/elements/bookVII)
  • Wolfram MathWorld, Merge Sort (mathworld.wolfram.com/MergeSort.html: proposed by John von Neumann in 1945, after Knuth 1998 p. 159)
  • Stanford CS "Tony Hoare: Quicksort" (cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/tony-hoare/quicksort.html: devised while a student at Moscow State University for Russian-English machine translation with a tape dictionary, programmed in Algol 60 and published 1961; the year 1959 from the Wikipedia Quicksort article and its cited sources)
  • Bodleian Libraries, Ada Lovelace (blogs.bodleian.ox.ac.uk/adalovelace: 1843 paper, Note G Bernoulli numbers)
  • Fourmilab text of Menabrea's Sketch with Lovelace's notes (fourmilab.ch/babbage/sketch.html)
  • Seven Bridges of Konigsberg (Euler 1736) and Tower of Hanoi (Edouard Lucas, 1883) checked against the Wikipedia articles and their cited sources.

Consulté le: 2026-10-17