Adloun

Plus courts chemins : Dijkstra

Cours complet · OCaml (option informatique), chapitre 16 · 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>16.1 Introduction et motivation

Le parcours en largeur (chapitre 15) trouve le plus court chemin… quand toutes les arêtes « coûtent » pareil. Mais sur un réseau routier, les routes ont des longueurs différentes : le plus court chemin n'est plus celui qui traverse le moins de villes, mais celui dont la somme des distances est minimale. Il faut des graphes pondérés et un algorithme adapté : c'est l'algorithme de Dijkstra, l'un des plus célèbres de l'informatique.

Ce chapitre clôt l'étude des graphes. On y voit pourquoi le BFS échoue sur un graphe pondéré, comment Dijkstra construit les distances minimales depuis une source, pourquoi il exige des poids positifs (avec un contre-exemple), comment reconstruire le chemin, et deux variantes (Floyd-Warshall pour toutes les paires, Bellman-Ford pour les poids négatifs).

iRemarqueUne « infinité » entière

Faute de distance infinie en machine, on choisit une constante infini plus grande que toute distance réalisable (par exemple 1_000_000 si les distances restent petites, ou la somme de tous les poids plus un). dist.(v) = infini signifie alors « v pas encore atteint ».

16.2 Graphes pondérés et relâchement

On représente un graphe pondéré par des listes d'adjacence portant le poids : adj.(u) est la liste des couples (v, poids) pour chaque arête u -&gt; v.

L'opération de base est le relâchement d'une arête (u, v, poids) : si passer par u améliore la distance estimée à v, on la met à jour.


(* si dist.(u) + poids < dist.(v), on a trouvé mieux pour v *)
if dist.(u) + poids < dist.(v) then dist.(v) <- dist.(u) + poids
ImportantPourquoi le BFS ne suffit plus

Le BFS traite les sommets par nombre d'arêtes croissant — ce qui ne correspond plus à la distance pondérée. Un chemin de deux arêtes courtes peut être plus court qu'un chemin d'une seule arête longue. Il faut traiter les sommets par distance pondérée croissante : c'est l'idée de Dijkstra.

16.3 L'algorithme de Dijkstra

Méthode : Principe de Dijkstra (poids positifs)

On maintient une distance estimée dist.(v) pour chaque sommet, initialement 0 pour la source et infini ailleurs. On répète :

  • choisir le sommet u non encore traité de dist minimale ;
  • le marquer traité — sa distance est désormais définitive ;
  • relâcher toutes ses arêtes sortantes.

Comme les poids sont positifs, le sommet le plus proche non traité ne pourra jamais être amélioré ensuite : on peut le figer.


let dijkstra adj depart =
  let n = Array.length adj in
  let infini = 1_000_000 in
  let dist = Array.make n infini in
  let traite = Array.make n false in
  dist.(depart) <- 0;
  for _i = 0 to n - 1 do
    (* 1. sommet non traité de distance minimale *)
    let u = ref (-1) in
    for v = 0 to n - 1 do
      if not traite.(v) && dist.(v) < infini
         && (!u = -1 || dist.(v) < dist.(!u)) then u := v
    done;
    if !u <> -1 then begin
      traite.(!u) <- true;                       (* 2. distance définitive *)
      List.iter (fun (v, poids) ->               (* 3. relâcher les voisins *)
        if dist.(!u) + poids < dist.(v) then dist.(v) <- dist.(!u) + poids
      ) adj.(!u)
    end
  done;
  dist

Complexité : Dijkstra en

À chaque tour, on cherche le minimum en parcourant les n sommets ; il y a n tours, d'où . Une file de priorité (tas) abaisserait ce coût à , mais elle dépasse le programme : la version ci-dessus, parfaitement correcte, est celle à connaître.

AttentionDijkstra exige des poids positifs

Avec une arête de poids négatif, Dijkstra peut se tromper : il fige une distance qu'un détour par une arête négative rendrait pourtant plus courte. Exemple : arêtes 0 -&gt; 1 (poids 2), 0 -&gt; 2 (poids 5), 2 -&gt; 1 (poids -4), 1 -&gt; 3 (poids 1). Dijkstra fige 1 à la distance 2, puis 3 à la distance 3, avant de traiter 2 ; le détour 0 -&gt; 2 -&gt; 1 -&gt; 3 coûte pourtant 5 - 4 + 1 = 2. Pour les poids négatifs (sans cycle négatif), on emploie Bellman-Ford.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>16.4 Exercices résolus

Niveau (application directe du cours)

Exercice 1 : Graphe pondéré

Construire un graphe pondéré en listes d'adjacence (int * int) list array et écrire ajoute_arc adj u v poids.

Démonstration

let pondere_vide n = Array.make n []

let ajoute_arc adj u v poids = adj.(u) <- (v, poids) :: adj.(u)

let ajoute_arete adj u v poids =        (* non orienté *)
  adj.(u) <- (v, poids) :: adj.(u);
  adj.(v) <- (u, poids) :: adj.(v)

Chaque voisin est un couple (sommet, poids). C'est l'extension naturelle des listes d'adjacence du chapitre 14.

Exercice 2 : Longueur d'un chemin

Écrire longueur adj chemin : la somme des poids le long d'un chemin donné (liste de sommets), ou failwith si une arête manque.

Démonstration

let rec poids_arc adj u v =
  match adj.(u) with
  | [] -> failwith "arête absente"
  | (w, p) :: reste -> if w = v then p else poids_arc_liste reste v
and poids_arc_liste l v =
  match l with
  | [] -> failwith "arête absente"
  | (w, p) :: reste -> if w = v then p else poids_arc_liste reste v

let rec longueur adj chemin =
  match chemin with
  | [] | [_] -> 0
  | u :: (v :: _ as reste) -> poids_arc adj u v + longueur adj reste

On additionne le poids de chaque arête consécutive du chemin. Le motif u :: (v :: _ as reste) (chapitre 9) donne les deux premiers sommets et la queue.

Exercice 3 : Le pas de relâchement

Expliquer ce que calcule if dist.(u) + poids &lt; dist.(v) then dist.(v) &lt;- dist.(u) + poids et pourquoi c'est le cœur de Dijkstra.

Démonstration

Le relâchement teste si atteindre v en passant par u (distance dist.(u) jusqu'à u, plus le poids de l'arête) est meilleur que la meilleure distance connue à v ; si oui, on met à jour. Dijkstra n'est qu'une discipline de relâchements : relâcher les arêtes des sommets dans l'ordre de leur distance croissante garantit qu'on ne relâche jamais à partir d'une distance non définitive.

Niveau (raisonnement intermédiaire)

Exercice 4 : Dijkstra

Écrire dijkstra adj depart renvoyant le tableau des distances minimales.

Démonstration

let dijkstra adj depart =
  let n = Array.length adj in
  let infini = 1_000_000 in
  let dist = Array.make n infini in
  let traite = Array.make n false in
  dist.(depart) <- 0;
  for _i = 0 to n - 1 do
    let u = ref (-1) in
    for v = 0 to n - 1 do
      if not traite.(v) && dist.(v) < infini
         && (!u = -1 || dist.(v) < dist.(!u)) then u := v
    done;
    if !u <> -1 then begin
      traite.(!u) <- true;
      List.iter (fun (v, poids) ->
        if dist.(!u) + poids < dist.(v) then dist.(v) <- dist.(!u) + poids
      ) adj.(!u)
    end
  done;
  dist

À chaque tour on fige le sommet non traité le plus proche, puis on relâche ses voisins. Les sommets inatteignables conservent infini.

Exercice 5 : Distance entre deux sommets

Écrire distance adj a b : int option (None si b est inatteignable).

Démonstration

let distance adj a b =
  let dist = dijkstra adj a in
  if dist.(b) >= 1_000_000 then None else Some dist.(b)

On lance Dijkstra depuis a et on lit la case b. La valeur sentinelle infini (restée inchangée) signale l'inaccessibilité, qu'on traduit en None.

Exercice 6 : Poids unitaires

Montrer que, si tous les poids valent 1, Dijkstra donne les mêmes distances qu'un BFS.

Démonstration

Avec des poids tous égaux à 1, la distance pondérée d'un chemin est son nombre d'arêtes. Choisir le sommet non traité de distance minimale revient alors à traiter les sommets par nombre d'arêtes croissant — exactement l'ordre du BFS. Dijkstra généralise donc le BFS aux poids quelconques (positifs) : pour des poids unitaires, le BFS (en ) est préférable car plus simple et plus rapide.

Exercice 7 : Le contre-exemple des poids négatifs

Sur le graphe 0 -&gt; 1 (poids 2), 0 -&gt; 2 (poids 5), 2 -&gt; 1 (poids -4), 1 -&gt; 3 (poids 1), donner les distances que calcule la fonction dijkstra de l'exercice 4 depuis 0, et les vraies.

Démonstration

Dijkstra traite d'abord 0 (relâche : dist.(1) = 2, dist.(2) = 5), puis fige 1 (le plus proche, 2) et relâche dist.(3) = 3, puis fige 3 (3 &lt; 5), et traite 2 en dernier. Ce dernier relâchement ramène dist.(1) à 5 - 4 = 1 (le code relâche sans tester traite), mais 3, déjà figé, n'est plus revu : la fonction renvoie [|0; 1; 5; 3|], alors que la vraie distance de 0 à 3 est 5 - 4 + 1 = 2. Sans l'arc 1 -&gt; 3, l'erreur resterait invisible : le relâchement tardif corrigerait dist.(1) seul. Dijkstra se trompe dès qu'un sommet figé trop tôt a transmis une distance fausse à d'autres : c'est pourquoi Dijkstra suppose les poids positifs.

Niveau (approfondissement)

Exercice 8 : Reconstruire le chemin

Modifier Dijkstra pour renvoyer aussi un tableau pere, et écrire chemin adj depart arrivee : int list option.

Démonstration

let dijkstra_peres adj depart =
  let n = Array.length adj in
  let infini = 1_000_000 in
  let dist = Array.make n infini and pere = Array.make n (-1) in
  let traite = Array.make n false in
  dist.(depart) <- 0;
  for _i = 0 to n - 1 do
    let u = ref (-1) in
    for v = 0 to n - 1 do
      if not traite.(v) && dist.(v) < infini
         && (!u = -1 || dist.(v) < dist.(!u)) then u := v
    done;
    if !u <> -1 then begin
      traite.(!u) <- true;
      List.iter (fun (v, poids) ->
        if dist.(!u) + poids < dist.(v) then begin
          dist.(v) <- dist.(!u) + poids;
          pere.(v) <- !u
        end
      ) adj.(!u)
    end
  done;
  (dist, pere)

let chemin adj depart arrivee =
  let (dist, pere) = dijkstra_peres adj depart in
  if dist.(arrivee) >= 1_000_000 then None
  else
    let rec remonte s =
      if s = depart then [depart] else remonte pere.(s) @ [s]
    in
    Some (remonte arrivee)

À chaque relâchement réussi, on note le prédécesseur pere.(v) &lt;- u. On reconstruit le chemin en remontant les pères — exactement comme pour le BFS (chapitre 15) et la programmation dynamique (chapitre 13).

Exercice 9 : Floyd-Warshall (toutes les paires)

Écrire floyd cout qui calcule les distances minimales entre toutes les paires de sommets (cout : matrice des poids, infini si pas d'arête, 0 sur la diagonale).

Démonstration

let floyd cout =
  let n = Array.length cout in
  let infini = 1_000_000 in
  let d = Array.make_matrix n n 0 in
  for i = 0 to n - 1 do
    for j = 0 to n - 1 do d.(i).(j) <- cout.(i).(j) done
  done;
  for k = 0 to n - 1 do
    for i = 0 to n - 1 do
      for j = 0 to n - 1 do
        if d.(i).(k) < infini && d.(k).(j) < infini
           && d.(i).(k) + d.(k).(j) < d.(i).(j) then
          d.(i).(j) <- d.(i).(k) + d.(k).(j)
      done
    done
  done;
  d

C'est de la programmation dynamique (chapitre 13) sur les graphes : d.(i).(j) devient le plus court chemin de i à j n'empruntant que des sommets intermédiaires &lt; k+1 ; la boucle sur k (la plus externe) autorise un intermédiaire de plus à chaque fois. Coût ; gère les poids négatifs (sans cycle négatif). La garde évite de « passer par l'infini ».

Exercice 10 : Bellman-Ford (poids négatifs)

Écrire bellman_ford n aretes depart (arêtes (u, v, poids), poids éventuellement négatifs, sans cycle de poids négatif).

Démonstration

let bellman_ford n aretes depart =
  let infini = 1_000_000 in
  let dist = Array.make n infini in
  dist.(depart) <- 0;
  for _i = 1 to n - 1 do                 (* n-1 passes *)
    List.iter (fun (u, v, poids) ->
      if dist.(u) < infini && dist.(u) + poids < dist.(v) then
        dist.(v) <- dist.(u) + poids
    ) aretes
  done;
  dist

On relâche toutes les arêtes, n-1 fois de suite. Comme un plus court chemin a au plus n-1 arêtes, n-1 passes suffisent à propager les bonnes distances. Plus lent que Dijkstra () mais il gère les poids négatifs (et une n-ième passe qui améliorerait encore révélerait un cycle de poids négatif).

Synthèse du chapitre (à retenir)
  • Graphe pondéré : listes d'adjacence (voisin, poids) (ou matrice des poids). infini = sentinelle « pas atteint ».
  • Relâchement : if dist.(u) + poids &lt; dist.(v) then dist.(v) &lt;- ... — le cœur de tous ces algorithmes.
  • Dijkstra (poids positifs) : figer le sommet non traité le plus proche, relâcher ses voisins ; (version tableau). Généralise le BFS (qui suffit pour des poids unitaires).
  • Poids négatifs : Dijkstra échoue (contre-exemple) Bellman-Ford ( relâchements de toutes les arêtes, ).
  • Toutes les paires : Floyd-Warshall (), programmation dynamique sur les graphes (boucle k externe).
  • Reconstruire le chemin : tableau des pere mis à jour à chaque relâchement réussi, puis remontée.

16.5 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 — Graphes pondérés.
Thème B — Dijkstra.
Thème C — Variantes du chemin.
Thème D — Toutes paires et négatifs.

Continuer sur Adloun : animation, QCM, fiches, exercices