Adloun

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
ImportantMarquer à l'enfilement

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.

ImportantBFS calcule les plus courts chemins

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)

Exercice 1 : Parcours en profondeur

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

Exercice 2 : Sommets accessibles

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

Exercice 3 : Parcours en largeur

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

Exercice 4 : Distances depuis un sommet

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

Exercice 5 : Composantes connexes

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

Exercice 6 : Deux sommets sont-ils reliés ?

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

Exercice 7 : Détecter un cycle

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

Exercice 8 : Graphe biparti

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

Exercice 9 : Plus court chemin (reconstruit)

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

Exercice 10 : DFS itératif avec pile explicite

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.

Synthèse du chapitre (à retenir)
  • 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 Stack explicite ; 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.
Thème B — Distances et chemins.
Thème C — Propriétés structurelles.
Thème D — Modélisation.

Continuer sur Adloun : animation, QCM, fiches, exercices