Parcours de graphes
Cours complet · OCaml (option informatique), chapitre 15 · prépas MPSI et MP, option informatique
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>15.1 Introduction et motivation
Représenter un graphe (chapitre 14) ne suffit pas : il faut savoir le parcourir — visiter systématiquement tous les sommets atteignables depuis un point de départ. C'est l'opération de base dont découlent quantité d'algorithmes : tester si deux sommets sont reliés, compter les composantes, trouver le plus court chemin en nombre d'arêtes, détecter un cycle, colorier un graphe.
Deux stratégies s'opposent et se complètent : le parcours en profondeur (DFS), qui s'enfonce le plus loin possible avant de revenir, et le parcours en largeur (BFS), qui explore par cercles concentriques autour du départ. Le premier s'écrit naturellement par récursion (ou avec une pile) ; le second avec une file — les structures du chapitre 8. Tout au long, un tableau de marquage évite de visiter deux fois le même sommet (donc de tourner en rond). On suppose les graphes donnés en listes d'adjacence (int list array).
15.2 Le parcours en profondeur (DFS)
Principe : visiter un sommet, le marquer, puis visiter récursivement chacun de ses voisins non encore visités. On s'enfonce dans une branche jusqu'au bout avant d'explorer les suivantes.
let parcours_profondeur adj depart =
let n = Array.length adj in
let vu = Array.make n false in
let ordre = ref [] in
let rec visite s =
if not vu.(s) then begin
vu.(s) <- true;
ordre := s :: !ordre;
List.iter visite adj.(s)
end
in
visite depart;
!ordre (* sommets visités, en ordre inverse de découverte *)
Méthode : Le marquage est indispensable
Sans le tableau vu, le parcours d'un graphe avec cycle bouclerait indéfiniment. On marque un sommet dès qu'on le visite, et l'on ne visite que les sommets non marqués. C'est l'analogue, sur un graphe, du vu du labyrinthe (chapitre 11) — un graphe étant la généralisation d'une grille.
La récursion utilise implicitement la pile d'appels. On peut rendre cette pile explicite (voir exercice 10), ce qui donne une version itérative équivalente.
15.3 Le parcours en largeur (BFS)
Principe : explorer le départ, puis tous ses voisins (distance ), puis les voisins de ceux-ci (distance ), etc. On gère les sommets à traiter dans une file : premier découvert, premier exploré.
let parcours_largeur adj depart =
let n = Array.length adj in
let vu = Array.make n false in
let f = Queue.create () in
let ordre = ref [] in
vu.(depart) <- true;
Queue.push depart f;
while not (Queue.is_empty f) do
let s = Queue.pop f in
ordre := s :: !ordre;
List.iter (fun v ->
if not vu.(v) then begin
vu.(v) <- true; (* marquer DÈS l'ajout dans la file *)
Queue.push v f
end
) adj.(s)
done;
!ordre
En BFS, on marque un sommet au moment où on l'ajoute à la file, et non quand on le retire. Sinon, un même sommet pourrait être enfilé plusieurs fois (par plusieurs voisins) avant d'être traité, faussant le parcours. Cette règle est essentielle à la correction.
Parce qu'il explore par distance croissante, le BFS atteint chaque sommet par un chemin de nombre d'arêtes minimal. En enregistrant, pour chaque sommet, la distance du départ, on obtient les plus courts chemins en nombre d'arêtes — gratuitement.
let distances adj depart =
let n = Array.length adj in
let dist = Array.make n (-1) in (* -1 : non atteint *)
let f = Queue.create () in
dist.(depart) <- 0;
Queue.push depart f;
while not (Queue.is_empty f) do
let s = Queue.pop f in
List.iter (fun v ->
if dist.(v) = -1 then begin
dist.(v) <- dist.(s) + 1;
Queue.push v f
end
) adj.(s)
done;
dist
15.4 Accessibilité et composantes connexes
Un parcours depuis s visite exactement les sommets atteignables depuis s. En relançant un parcours sur chaque sommet encore non visité, on dénombre les composantes connexes.
let nb_composantes adj =
let n = Array.length adj in
let vu = Array.make n false in
let rec visite s =
if not vu.(s) then begin
vu.(s) <- true;
List.iter visite adj.(s)
end
in
let c = ref 0 in
for s = 0 to n - 1 do
if not vu.(s) then begin
c := !c + 1; (* nouveau sommet non vu : nouvelle composante *)
visite s
end
done;
!c
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>15.5 Exercices résolus
Niveau (application directe du cours)
Écrire parcours_profondeur adj depart renvoyant la liste des sommets visités.
Démonstration
let parcours_profondeur adj depart =
let n = Array.length adj in
let vu = Array.make n false in
let ordre = ref [] in
let rec visite s =
if not vu.(s) then begin
vu.(s) <- true;
ordre := s :: !ordre;
List.iter visite adj.(s)
end
in
visite depart;
!ordre
La récursion s'enfonce dans le premier voisin non vu, puis remonte. ordre accumule les sommets dans l'ordre inverse de découverte (chaque nouveau est mis en tête).
Écrire accessibles adj depart renvoyant le tableau vu (vu.(i) = i atteignable depuis depart).
Démonstration
let accessibles adj depart =
let n = Array.length adj in
let vu = Array.make n false in
let rec visite s =
if not vu.(s) then begin
vu.(s) <- true;
List.iter visite adj.(s)
end
in
visite depart;
vu
Le tableau vu en fin de parcours marque exactement les sommets atteignables. On a retiré l'accumulation de l'ordre : seule l'atteignabilité importe.
Écrire parcours_largeur adj depart (avec une file).
Démonstration
let parcours_largeur adj depart =
let n = Array.length adj in
let vu = Array.make n false in
let f = Queue.create () in
let ordre = ref [] in
vu.(depart) <- true;
Queue.push depart f;
while not (Queue.is_empty f) do
let s = Queue.pop f in
ordre := s :: !ordre;
List.iter (fun v ->
if not vu.(v) then begin vu.(v) <- true; Queue.push v f end
) adj.(s)
done;
!ordre
La file impose l'ordre « premier découvert, premier traité » : les sommets sortent par distance croissante au départ.
Niveau (raisonnement intermédiaire)
Écrire distances adj depart et expliquer pourquoi il donne les plus courts chemins.
Démonstration
let distances adj depart =
let n = Array.length adj in
let dist = Array.make n (-1) in
let f = Queue.create () in
dist.(depart) <- 0;
Queue.push depart f;
while not (Queue.is_empty f) do
let s = Queue.pop f in
List.iter (fun v ->
if dist.(v) = -1 then begin dist.(v) <- dist.(s) + 1; Queue.push v f end
) adj.(s)
done;
dist
Le BFS traite les sommets par distance croissante : quand on atteint v pour la première fois, c'est par le plus court chemin, et dist.(v) = dist.(s) + 1. Les sommets inatteignables gardent -1.
Écrire nb_composantes adj.
Démonstration
let nb_composantes adj =
let n = Array.length adj in
let vu = Array.make n false in
let rec visite s =
if not vu.(s) then begin vu.(s) <- true; List.iter visite adj.(s) end
in
let c = ref 0 in
for s = 0 to n - 1 do
if not vu.(s) then begin c := !c + 1; visite s end
done;
!c
Chaque sommet encore non vu lance un parcours qui marque toute sa composante ; on en compte ainsi le nombre. Un graphe connexe a une seule composante.
Écrire relies adj a b : bool (existe-t-il un chemin de a à b ?).
Démonstration
let relies adj a b =
let vu = accessibles adj a in
vu.(b)
On parcourt depuis a et l'on regarde si b a été atteint. Réutiliser accessibles rend la fonction triviale : un bon parcours est une brique réutilisable.
Niveau (approfondissement)
Écrire a_cycle adj pour un graphe non orienté.
Démonstration
let a_cycle adj =
let n = Array.length adj in
let vu = Array.make n false in
let rec visite s pere =
vu.(s) <- true;
let rec verifie voisins =
match voisins with
| [] -> false
| v :: reste ->
if not vu.(v) then visite v s || verifie reste
else if v <> pere then true (* déjà vu, <> père : cycle ! *)
else verifie reste
in
verifie adj.(s)
in
let rec balaye s =
if s = n then false
else if not vu.(s) then visite s (-1) || balaye (s + 1)
else balaye (s + 1)
in
balaye 0
On effectue un DFS en retenant le pere (le sommet d'où l'on vient). Rencontrer un voisin déjà vu qui n'est pas le père signale un cycle (on est revenu sur ses pas par un autre chemin). Le balaye relance sur chaque composante.
Écrire est_biparti adj : peut-on colorier les sommets en deux couleurs sans que deux voisins partagent la même ?
Démonstration
let est_biparti adj =
let n = Array.length adj in
let couleur = Array.make n (-1) in (* -1 non colorié, 0 ou 1 *)
let ok = ref true in
let f = Queue.create () in
for depart = 0 to n - 1 do
if couleur.(depart) = -1 then begin
couleur.(depart) <- 0;
Queue.push depart f;
while not (Queue.is_empty f) do
let s = Queue.pop f in
List.iter (fun v ->
if couleur.(v) = -1 then begin
couleur.(v) <- 1 - couleur.(s); Queue.push v f
end
else if couleur.(v) = couleur.(s) then ok := false
) adj.(s)
done
end
done;
!ok
Un BFS colorie chaque sommet de la couleur opposée à son prédécesseur. Si l'on rencontre un voisin déjà colorié de la même couleur, le graphe n'est pas biparti (il contient un cycle impair). La boucle externe traite toutes les composantes.
Écrire plus_court_chemin adj depart arrivee : int list option renvoyant un plus court chemin (en nombre d'arêtes), ou None.
Démonstration
let plus_court_chemin adj depart arrivee =
let n = Array.length adj in
let pere = Array.make n (-1) in
let vu = Array.make n false in
let f = Queue.create () in
vu.(depart) <- true;
Queue.push depart f;
while not (Queue.is_empty f) do
let s = Queue.pop f in
List.iter (fun v ->
if not vu.(v) then begin
vu.(v) <- true; pere.(v) <- s; Queue.push v f
end
) adj.(s)
done;
if not vu.(arrivee) then None
else
let rec remonte s =
if s = depart then [depart] else remonte pere.(s) @ [s]
in
Some (remonte arrivee)
On enregistre, lors du BFS, le pere par lequel chaque sommet a été atteint (sur son plus court chemin). On reconstruit ensuite le chemin en remontant les pères de arrivee jusqu'au depart. Même idée que la reconstruction en programmation dynamique (chapitre 13).
Réécrire le parcours en profondeur avec une Stack explicite, sans récursion.
Démonstration
let dfs_iteratif adj depart =
let n = Array.length adj in
let vu = Array.make n false in
let p = Stack.create () in
let ordre = ref [] in
Stack.push depart p;
while not (Stack.is_empty p) do
let s = Stack.pop p in
if not vu.(s) then begin
vu.(s) <- true;
ordre := s :: !ordre;
List.iter (fun v -> if not vu.(v) then Stack.push v p) adj.(s)
end
done;
!ordre
La pile remplace la pile d'appels de la version récursive. Subtilité : un sommet peut être empilé plusieurs fois (par différents voisins) ; on le marque donc au moment où on le dépile, et l'on ignore les doublons déjà vus. Remplacer la Stack par une Queue transformerait ce DFS en BFS — preuve que c'est la structure (pile ou file) qui distingue les deux parcours.
- Marquer les sommets visités (
vu) est indispensable : sans cela, un graphe avec cycle ferait boucler le parcours. - DFS (profondeur) : récursif (pile d'appels) ou avec une
Stackexplicite ; s'enfonce avant de revenir. - BFS (largeur) : avec une
Queue; explore par distance croissante. Marquer à l'enfilement. Donne les plus courts chemins en nombre d'arêtes. - C'est la structure (pile DFS, file BFS) qui distingue les deux parcours, à code presque identique.
- Applications : accessibilité (un parcours), composantes connexes (un parcours par composante), distances et plus court chemin (BFS + tableau des pères), cycle (DFS + père), bipartisme (BFS bicolore).
15.6 Exercices d'entraînement
Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.
Thème A — Parcours de base.
- [11.] Écrire
taille_composante adj s: le nombre de sommets atteignables depuiss. - [12.] Écrire
est_connexe adj(une seule composante). - [13.] Adapter
parcours_profondeurpour une matrice d'adjacence au lieu des listes.
Thème B — Distances et chemins.
- [14.] Écrire
excentricite adj s: la plus grande distance desà un sommet atteignable. - [15.]
diametre adj: la plus grande distance entre deux sommets (lancer un BFS depuis chacun). - [16.]
degres_separation: sur un graphe d'amitiés, la distance entre deux personnes (petit monde).
Thème C — Propriétés structurelles.
- [17.] Détecter un cycle dans un graphe orienté (suivre les sommets « en cours d'exploration »).
- [18.]
liste_composantes adj: la liste des composantes, chacune comme liste de ses sommets. - [19.] Vérifier qu'un graphe est un arbre (connexe et sans cycle, ou arêtes et connexe).
Thème D — Modélisation.
- [20.] Échelle de mots : peut-on passer d'un mot à un autre en changeant une lettre à la fois (chaque mot intermédiaire valide) ? Modéliser par un graphe et faire un BFS.
- [21.] Labyrinthe (chapitre 11) revu comme un graphe : retrouver
accessiblepar un parcours. - [22.] Discuter : quand préférer DFS à BFS, et inversement ?