Plus courts chemins : Dijkstra et au-delà
Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 14 · prépas scientifiques, tronc commun
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>14.1 Introduction et motivation
« Itinéraire le plus rapide vers… » : la phrase la plus tapée des applications de navigation est une question de graphe pondéré — et la réponse, calculée en quelques millisecondes sur un réseau routier de plusieurs millions de carrefours, sort d'un algorithme conçu en 1956 par Edsger Dijkstra, en vingt minutes dit-il, à la terrasse d'un café d'Amsterdam. Le chapitre 13 savait minimiser le nombre d'étapes (parcours en largeur) ; dès que les arcs portent des poids — kilomètres, minutes, coûts — ce n'est plus la bonne réponse : deux petites étapes valent mieux qu'une grande, et il faut un algorithme qui additionne les poids.
L'algorithme de Dijkstra est ce qu'il faut : une généralisation du parcours en largeur où la file d'attente ne sert plus les sommets par ancienneté mais par distance provisoire croissante — une file de priorité, qu'on implémentera naïvement comme le permet le programme. Sa correction repose sur un invariant d'une élégance rare, et sur une hypothèse à ne jamais oublier : les poids sont positifs. Le chapitre s'achève sur une ouverture : l'algorithme A, variante de Dijkstra guidée par une heuristique* — l'idée qui fait passer de « explorer en rond » à « viser la cible », et la première rencontre du cours avec une notion qui irrigue toute l'intelligence artificielle.
14.2 Le problème, et pourquoi le BFS ne suffit plus
Dans un graphe pondéré à poids positifs, le poids d'un chemin est la somme des poids de ses arcs, et la distance est le poids minimal d'un chemin de à (infinie si aucun n'existe). Le problème du chapitre : calculer toutes les distances depuis une source — et les chemins qui les réalisent.
Le parcours en largeur répond « directement » ( étape) ; mais ce chemin pèse , quand pèse . Compter les arcs et additionner les poids sont deux problèmes distincts — le BFS résout le premier, Dijkstra le second (et les confond exactement quand tous les poids valent : le BFS est le cas particulier).
14.3 L'algorithme de Dijkstra
14.3.1 L'idée : figer les sommets par distance croissante
L'algorithme entretient pour chaque sommet une distance provisoire — le poids du meilleur chemin connu jusqu'ici — et fige les sommets un par un : à chaque tour, le sommet non figé de plus petite distance provisoire est promu définitif (on prouvera que sa provisoire est alors exacte), et ses voisins voient leur provisoire s'améliorer s'il offre un raccourci. C'est une stratégie gloutonne (chapitre 7 !) — et, pour une fois, prouvablement optimale.
def dijkstra(R: dict, source) -> tuple:
"""R : graphe pondéré {u: {v: poids}}, poids > 0.
Renvoie (dist, pere) : distances exactes depuis source, et l'arbre
des pères pour reconstruire les chemins."""
dist = {s: float("inf") for s in R}
dist[source] = 0
pere = {}
a_traiter = set(R) # les sommets non encore figés
while a_traiter:
# EXTRACTION DU MINIMUM (file de priorité naïve : un parcours)
s = None
for x in a_traiter:
if s is None or dist[x] < dist[s]:
s = x
if dist[s] == float("inf"):
break # le reste est inaccessible
a_traiter.remove(s) # s est FIGÉ : dist[s] est exacte
# RELÂCHEMENT des arcs sortants de s (vers les sommets non figés)
for v in R[s]:
if v in a_traiter and dist[s] + R[s][v] < dist[v]:
dist[v] = dist[s] + R[s][v] # un raccourci via s !
pere[v] = s
return (dist, pere)
Les deux gestes ont un nom : l'extraction du minimum (la file de priorité — ici naïve, un simple parcours des candidats, conformément au programme) et le relâchement d'un arc (relaxation : améliorer la provisoire d'un voisin si l'on a trouvé mieux).
| tour | figé | |||||
|---|---|---|---|---|---|---|
| init | — | |||||
| 1 | ||||||
| 2 | ||||||
| 3 | ||||||
| 4 | ||||||
| 5 |
Lecture du tour 2 : (provisoire , le minimum) est figé ; ses voisins se relâchent — passe de à (le détour par bat la route directe !) et reçoit . Au tour 4, relâche : — le chemin final est , de poids , reconstruit par les pères : , , , .
14.3.2 La preuve
Si tous les poids sont positifs, alors au moment où un sommet est figé, sa distance provisoire est exacte : .
Démonstration
Invariant : à chaque tour, (i) pour tout sommet figé , ; (ii) pour tout sommet non figé , est le poids du meilleur chemin de la source à dont tous les sommets intermédiaires sont figés. L'initialisation est claire (, le reste ).
Le pas crucial : soit le sommet promu (minimum des provisoires). Supposons par l'absurde qu'un chemin strictement meilleur que existe de la source vers . Ce chemin quitte à un moment la zone figée : soit son premier arc avec figé et non figé. Alors :
la deuxième inégalité parce que le reste du chemin , de jusqu'à , a un poids positif ou nul — c'est ici, et seulement ici, que la positivité des poids travaille. Mais contredit le choix de comme minimum. Donc : (i) s'étend à . La mise à jour des voisins de rétablit (ii), et l'invariant se propage. Terminaison : chaque tour fige un sommet — variant : le cardinal de a_traiter.
Avec un seul arc négatif, l'argument s'effondre — et l'algorithme aussi. Sur le graphe , , : Dijkstra fige à la distance … alors que pèse .
Le sommet a été figé trop tôt : « le reste du chemin pèse » était faux. Les graphes à poids négatifs relèvent d'autres algorithmes (programme de seconde année) ; retenir le réflexe : avant d'invoquer Dijkstra, vérifier la positivité — c'est une précondition au sens du chapitre 1, et elle mérite son assertion.
Complexité : Dijkstra naïf
Chaque tour extrait un minimum par balayage des non-figés () puis relâche les arcs sortants du promu. Au total : extractions en , et chaque arc relâché une seule fois — coût . Largement suffisant pour des graphes de quelques milliers de sommets. Pour les graphes routiers à sommets, l'extraction naïve est le goulot : une vraie file de priorité (un arbre binaire dit tas, structure de seconde année) la rend logarithmique et amène le tout à — l'algorithme ne change pas, seule la collection change : le leitmotiv du cours.
14.4 A* : Dijkstra qui sait où il va
14.4.1 L'heuristique
Dijkstra explore en rond : il fige les sommets par distance croissante à la source, dans toutes les directions — y compris à l'opposé de la cible. Quand on cherche le chemin de Paris à Marseille, il aura figé Lille et Brest avant d'atteindre Lyon. L'algorithme A* (lire « A étoile ») corrige cette myopie en exploitant une information supplémentaire : une heuristique , estimation du coût restant de à la cible.
A* est l'algorithme de Dijkstra où l'extraction du minimum ne se fait plus selon , mais selon la priorité
et où l'on s'arrête quand la cible est figée. Pour un graphe géographique, l'heuristique naturelle est la distance à vol d'oiseau jusqu'à la cible : facile à calculer, et jamais supérieure à la vraie distance par les routes. Une heuristique qui ne surestime jamais le coût restant est dite admissible — et l'on admet le théorème : avec une heuristique admissible et cohérente, A renvoie un plus court chemin exact. Cohérente veut dire que pour tout arc : l'estimation ne baisse jamais plus vite que le coût réellement parcouru. Le vol d'oiseau et la distance de Manhattan sur une grille le sont ; une heuristique seulement admissible ne suffit pas à la version qui fige les sommets, qui peut alors rendre un chemin trop long. Cas particulier limpide : est admissible, et A avec est Dijkstra.
Sur une grille avec obstacles (le labyrinthe du chapitre 13, version pondérée), cherchons un chemin du coin haut-gauche au coin bas-droit, avec pour heuristique la distance dite de Manhattan (admissible : chaque pas réduit cette somme d'au plus ). Le comportement observé — l'expérience à faire, exercice 8 — : Dijkstra fige un disque de cases autour du départ ; A fige un fuseau étiré vers la cible, plusieurs fois plus petit. Même chemin trouvé, même garantie, une fraction du travail : l'heuristique ne change pas la réponse, elle change où l'on regarde*.
Le mot heuristique a ici un sens précis — une estimation qui guide la recherche sans en compromettre l'exactitude (si elle est admissible et cohérente) — différent de celui du chapitre 7, où le glouton non prouvé était une heuristique au rabais (réponse approchée). A offre le meilleur des deux mondes : la vitesse d'une exploration orientée, la garantie d'un théorème. C'est l'algorithme des jeux vidéo, des GPS, de la planification robotique — et la sensibilisation demandée par le programme s'arrête à cette idée : une connaissance du problème, injectée dans l'algorithme, concentre l'effort là où la solution a une chance d'être*.
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>14.5 Exercices résolus
Niveau (Application directe du cours)
Dérouler l'algorithme sur le graphe routier du chapitre 12 (Paris, Lyon, Lille, Nantes, Marseille) depuis Paris : tableau des distances provisoires tour par tour, ordre des figés, et le chemin Paris--Marseille.
Démonstration (Solution)
Init : Paris , le reste . Tour 1 : Paris figé ; relâchements — Lyon , Lille , Nantes . Tour 2 : minimum Lille (), figée ; son seul voisin Paris est figé : rien. Tour 3 : Nantes () figée ; rien de mieux. Tour 4 : Lyon () figé ; relâchement — Marseille . Tour 5 : Marseille () figée. Distances finales : Lille , Nantes , Lyon , Marseille ; chemin vers Marseille par les pères : Paris Lyon Marseille. (Observer l'ordre des figés : par distance croissante à Paris, exactement comme le BFS figeait par nombre d'arêtes croissant — Dijkstra est un BFS qui a appris l'arithmétique.)
Sur le graphe à cinq sommets du cours (déroulé du tableau), vérifier à la main que le chemin direct (poids ) est battu, et expliquer à quel tour et par quel mécanisme l'algorithme s'en aperçoit. Que serait-il arrivé si avait été figé avant ?
Démonstration (Solution)
Au tour 1, reçoit la provisoire (la route directe). Au tour 2, — provisoire , plus petite que celle de — est figé en premier, et son relâchement découvre : la provisoire de est améliorée, et son père bascule de à . C'est tout le mécanisme : tant qu'un sommet n'est pas figé, sa provisoire reste contestable, et l'extraction par minimum garantit qu'on fige (qui peut offrir des raccourcis) avant (qui en bénéficie). Si l'on figeait d'abord — par exemple en traitant les sommets dans un ordre arbitraire — sa distance serait gelée à , fausse : c'est précisément l'erreur que commet Dijkstra sur poids négatifs, où le « bon ordre » n'existe plus. (Le contre-exemple négatif du cours et cet exercice sont le même phénomène, vu des deux côtés de l'hypothèse.)
Écrire chemin_vers(pere, source, cible) (réutiliser le chapitre 13), puis table_des_routes(R, source) qui affiche, pour chaque sommet accessible, sa distance et son chemin complet — l'écran d'un GPS.
Démonstration (Solution)
def table_des_routes(R: dict, source) -> None:
dist, pere = dijkstra(R, source)
for v in sorted(R, key=lambda x: dist[x]):
if dist[v] == float("inf"):
print(v, ": inaccessible")
elif v != source:
c = chemin(pere, source, v) # chapitre 13
print(v, ":", dist[v], "via", " -> ".join(map(str, c)))
Le tri par distance croissante reconstitue l'ordre des figés — agréable à lire et gratuit. Une seule exécution de Dijkstra livre les chemins vers toutes les destinations : l'algorithme est « une source vers tous », et c'est l'arbre des pères, construit en passant, qui contient toutes les routes. (L'arbre des pères de Dijkstra est l'analogue pondéré de l'arbre du BFS : un arbre couvrant des plus courts chemins — au sens du chapitre 13, un vrai arbre : arcs, pas de cycle.)
Niveau (Application avec raisonnement intermédiaire)
Ajouter à dijkstra la validation d'entrée du chapitre 10 (assertion de positivité des poids), puis construire le contre-exemple du cours et vérifier : (a) que sans l'assertion, l'algorithme rend une distance fausse en silence ; (b) qu'avec elle, le programme s'arrête au bon endroit.
Démonstration (Solution)
def dijkstra_sur(R: dict, source):
assert all(R[u][v] >= 0 for u in R for v in R[u]), "poids négatif !"
return dijkstra(R, source)
pieges = {"s": {"a": 2, "b": 3}, "a": {}, "b": {"a": -2}}
dist, _ = dijkstra(pieges, "s")
assert dist["a"] == 2 # FAUX silencieusement : la vraie distance est 1 !
# dijkstra_sur(pieges, "s") # AssertionError: poids négatif ! — l'arrêt propre
(a) L'exécution nue déroule sans broncher : (provisoire ) est figé avant (provisoire ), et le relâchement arrive trop tard — dist["a"] vaut , la vérité est . Aucun message : c'est l'erreur de logique du chapitre 1 dans toute sa noirceur, une précondition violée qui fait mentir le résultat (le parallèle exact de la dichotomie sur tableau non trié, chapitre 5). (b) L'assertion coûte — négligeable devant — et transforme le mensonge en arrêt immédiat et localisé. (Règle de conception : quand la correction d'un algorithme repose sur une hypothèse que l'appelant peut violer sans s'en douter, l'hypothèse mérite une assertion — le théorème du cours dit précisément où Dijkstra a le droit de mentir.)
Sur le triangle du cours (, ) : calculer le plus court chemin de à par BFS (chapitre 13) puis par Dijkstra, constater le désaccord, et démontrer la règle générale : sur un graphe dont tous les poids valent , Dijkstra et BFS donnent les mêmes distances.
Démonstration (Solution)
BFS : est à distance (une étape, l'arc direct). Dijkstra : via — les deux réponses sont « justes », elles ne mesurent pas la même chose (chapitre 12 : choisir ce que mesure le poids est un acte de modélisation).
Règle générale : à poids tous égaux à , le poids d'un chemin est son nombre d'arcs — les deux notions de distance coïncident par définition. Et les algorithmes aussi : dans Dijkstra, les provisoires ne prennent que des valeurs entières ou quand on traite les sommets de distance ; l'extraction du minimum sert alors les sommets exactement dans l'ordre où une file FIFO les aurait servis — Dijkstra dégénère en BFS, en payant ce que la file faisait en . (D'où la règle pratique : poids uniformes BFS, jamais Dijkstra — savoir reconnaître le cas particulier où l'outil simple suffit est aussi une compétence d'analyse.)
Reprendre la modélisation du métro (chapitre 12, exercice 10) : stations dédoublées par ligne, tronçons de min, correspondances de min. Construire un mini-réseau de deux lignes se croisant en une station, et vérifier que Dijkstra choisit d'éviter une correspondance quand un trajet direct à peine plus long existe.
Démonstration (Solution)
# Ligne 1 : A1 - B1 - C1 ; ligne 2 : D2 - B2 - E2 ; correspondance en B
M = {"A1": {"B1": 2}, "B1": {"A1": 2, "C1": 2, "B2": 5},
"C1": {"B1": 2},
"D2": {"B2": 2}, "B2": {"D2": 2, "E2": 2, "B1": 5},
"E2": {"B2": 2}}
dist, pere = dijkstra(M, "A1")
assert dist["C1"] == 4 # A1 -> B1 -> C1 : sans correspondance
assert dist["E2"] == 9 # A1 -> B1 -> (corresp. 5) -> B2 -> E2
assert chemin(pere, "A1", "E2") == ["A1", "B1", "B2", "E2"]
Le trajet vers E2 paie ses minutes de correspondance — c'est le dédoublement des stations qui rend ce coût exprimable : sans lui, « être à B » serait un seul sommet et changer de ligne serait gratuit, faussant tous les itinéraires. (Vérifier ensuite la sensibilité du modèle : passer la correspondance à minute, ou ajouter une ligne directe concurrente, et regarder les chemins basculer — un modèle se valide en le secouant, pas seulement en le lisant. C'est exactement ainsi que les calculateurs d'itinéraires réels arbitrent « le plus court » contre « le moins de changements » : en réglant des poids.)
Sur un réseau électrique pondéré par des capacités, on cherche le chemin de à qui maximise la capacité minimale de ses arcs (le « chemin du goulot le plus large »). Montrer que Dijkstra s'adapte : remplacer la somme par un et le minimum par un maximum — écrire la variante et la justifier en une phrase d'invariant.
Démonstration (Solution)
def chemin_du_goulot(R: dict, source) -> dict:
"""cap[v] = la plus grande capacité de goulot d'un chemin source -> v."""
cap = {s: 0 for s in R}
cap[source] = float("inf") # rien ne limite encore
a_traiter = set(R)
while a_traiter:
s = max(a_traiter, key=lambda x: cap[x]) # extraction du MAXIMUM
if cap[s] == 0:
break
a_traiter.remove(s)
for v in R[s]:
cap[v] = max(cap[v], min(cap[s], R[s][v])) # relâchement min/max
return cap
Justification-éclair, en miroir du théorème du cours : quand est extrait (le maximum des provisoires), tout autre chemin vers devrait sortir de la zone figée par un sommet de provisoire , et son goulot final ne peut qu'être encore plus étroit (le ne remonte jamais le long d'un chemin — l'analogue exact de « le reste du chemin pèse »). (La structure profonde de Dijkstra est là : une « addition » qui ne fait que dégrader le score le long d'un chemin, et une extraction par meilleur score — somme/min, min/max, produit de fiabilités/max (exercice 27 de la banque) : trois habillages du même théorème, et un exemple précoce de ce que « adapter un algorithme » veut dire — changer les opérateurs, re-vérifier l'invariant.)
Niveau (Raisonnement subtil ou plusieurs étapes)
Sur une grille sans obstacle, du coin au coin opposé : implémenter A avec l'heuristique de Manhattan, compter les sommets figés par Dijkstra et par A, comparer les distances trouvées, et expliquer la géométrie des deux zones explorées.
Démonstration (Solution)
def a_etoile_grille(p: int, q: int, depart: tuple, cible: tuple) -> tuple:
"""Grille p x q, pas de coût 1. Renvoie (distance, nb de sommets figés)."""
def h(x): # Manhattan : admissible ici
return abs(x[0] - cible[0]) + abs(x[1] - cible[1])
dist = {depart: 0}
figes = set()
candidats = {depart}
while candidats:
# priorité f = g + h ; à f égal, départager par h (le plus proche du but)
s = min(candidats, key=lambda x: (dist[x] + h(x), h(x)))
candidats.remove(s)
figes.add(s)
if s == cible:
return (dist[s], len(figes))
(i, j) = s
for v in [(i-1, j), (i+1, j), (i, j-1), (i, j+1)]:
if 0 <= v[0] < p and 0 <= v[1] < q and v not in figes:
if v not in dist or dist[s] + 1 < dist[v]:
dist[v] = dist[s] + 1
candidats.add(v)
return (None, len(figes))
Résultats mesurés sur : distance pour les deux (la garantie d'admissibilité tient) ; sommets figés — Dijkstra (la même fonction avec ) : , soit toute la grille (le « disque » de rayon la couvre entièrement) ; A : , une quasi-diagonale. Géométrie : Dijkstra fige par croissant — des cercles centrés au départ. Pour A, la grille vide est un cas extrême instructif : toute case y est sur un trajet parfait (en allant toujours vers le bas ou la droite, partout) — tous les candidats sont ex æquo, et c'est le départage par qui transforme cette indifférence en ligne droite vers la cible (sans lui, l'ordre des ex æquo est arbitraire et l'on fige la moitié de la grille : le tester !). Avec des obstacles, les cases des détours ont : A explore un fuseau autour des bons chemins, qui s'élargit juste ce qu'il faut pour contourner. (Facteur ici, davantage sur les grandes cartes : c'est l'écart entre « chercher partout » et « chercher là où ça peut être » — toute l'idée d'heuristique, chiffrée en une expérience de vingt lignes.)*
Reprendre la grille, mais avec l'heuristique non admissible (elle surestime). Montrer expérimentalement qu'A* explore encore moins… mais peut renvoyer un chemin sous-optimal — construire une configuration d'obstacles où cela se produit, et conclure sur le contrat de l'admissibilité.
Démonstration (Solution)
Avec Manhattan, la priorité écrase le terme : l'algorithme devient un fonceur quasi pur vers la cible (une « descente gloutonne », chapitre 7 !). Sur grille vide, aucun mal : le chemin direct est optimal. Le piège exige un détour payant dont l'entrée semble prometteuse. Configuration mesurée (grille , départ , cible , murs sur ) :
| # | # | |||
| # | ||||
| # | ||||
L'optimal (longueur ) bifurque tôt vers le bas, par la colonne . Le fonceur, lui, longe la rangée du haut en réduisant à toute vitesse, plonge par la colonne … et bute sur les murs et : il lui faut un crochet par — deux pas de détour. Revenir essayer la bifurcation manquée coûterait, à cause du facteur , une priorité artificiellement énorme : il fige la cible sans l'avoir explorée et rend un chemin de longueur quand l'optimal vaut — en ayant figé moins de sommets que l'A admissible ( contre : plus rapide, et faux). La preuve du cours montre où ça casse : « la cible figée a sa distance exacte » reposait sur le fait que tout candidat non figé porte vraie distance — vrai si ne surestime pas, faux sinon. Conclusion : l'admissibilité est le contrat qui sépare « plus rapide et toujours exact » (A admissible) de « encore plus rapide mais parfois faux » (heuristique gourmande) — le second a sa place (jeux temps réel), à condition d'être annoncé : c'est un choix de spécification, pas un détail. (Le triangle complet du semestre : Dijkstra prudent et prouvé, A guidé et prouvé, le fonceur véloce et faillible — trois points sur la frontière exactitude-vitesse, à choisir les yeux ouverts.)*
Trois énoncés d'itinéraire : (a) une ambulance part de l'hôpital — distances de l'hôpital vers tous les quartiers ; (b) chaque quartier doit connaître son temps d'accès vers l'hôpital (graphe orienté : sens uniques !) ; (c) un seul trajet domicile--travail. Pour chacun, dire comment employer Dijkstra (ou sa variante) au mieux, sans algorithme nouveau.
Démonstration (Solution)
(a) La forme native : un Dijkstra depuis l'hôpital donne tout — « une source vers tous » en une exécution.
(b) Le piège des sens uniques : les distances vers un sommet ne sont pas celles depuis lui. Plutôt que exécutions (une par quartier — !), retourner le graphe (chapitre 12, banque : inverser tous les arcs, ) et lancer un seul Dijkstra depuis l'hôpital dans le graphe inverse : la distance de l'hôpital à dans l'inverse est exactement celle de à l'hôpital dans l'original — un changement de point de vue vaut exécutions.
(c) Un seul couple source-cible : Dijkstra avec arrêt dès que la cible est figée (licite : le théorème dit que sa distance est alors exacte — c'est l'arrêt qu'A pratique déjà), et si le graphe s'y prête, A avec une heuristique admissible pour concentrer l'exploration. En moyenne, l'arrêt anticipé fige environ la moitié des sommets, A bien moins ; le pire cas reste celui de Dijkstra complet — l'amélioration sans la garantie chiffrée, comme tous les arrêts anticipés du cours. (Trois variations sans une ligne d'algorithme nouveau : choisir la source, retourner le graphe, s'arrêter à temps — l'essentiel du métier consiste souvent à bien poser le problème devant un outil qu'on possède déjà ; et c'est la note finale juste pour un cours d'algorithmique : les théorèmes sont peu nombreux, leurs usages innombrables.)*
- Distance pondérée : somme des poids du meilleur chemin ( nombre d'arcs : le BFS se trompe dès que les poids diffèrent — et lui est préférable quand ils sont tous égaux).
- Dijkstra (poids positifs) : distances provisoires, extraction du minimum (file de priorité — naïve : balayage , conforme au programme), sommet figé distance exacte, relâchement des arcs sortants () ; pères pour les chemins ; un glouton prouvé.
- La preuve : invariant « figés exacts provisoires meilleur chemin à intermédiaires figés » ; au moment de figer le minimum, tout chemin concurrent sort de la zone figée par un sommet de provisoire déjà supérieure, et son reste pèse — c'est là, et seulement là, que la positivité sert ; un arc négatif et l'algorithme ment en silence (figer trop tôt) : assertion de positivité en entrée.
- Coûts : naïf (suffisant jusqu'à sommets) ; avec une vraie file de priorité (seconde année) — même algorithme, autre collection. Une source tous en une exécution ; vers une cible : graphe inverse ; un seul couple : arrêt quand la cible est figée.
- A Dijkstra trié par , arrêt à la cible ; heuristique admissible ( coût restant réel : vol d'oiseau, Manhattan sur grille) et cohérente () exactitude garantie (admis) ; redonne Dijkstra ; géométrie : disque (Dijkstra) contre fuseau (A) — explorer là où la solution peut être ; surestimée plus rapide mais parfois faux : un contrat à annoncer.
- Adapter : la structure somme/min se transpose (/max : chemin du goulot le plus large) — changer les opérateurs, re-vérifier l'invariant.
14.6 Exercices d'entraînement
Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.
A. Dijkstra à la main et au clavier
- () Dérouler Dijkstra (tableau complet des provisoires) sur un graphe à six sommets donné, depuis deux sources différentes — vérifier que les arbres de pères diffèrent.
- () Écrire
distance(R, u, v)(un seul couple, avec arrêt anticipé) et son jeu de tests par partitionnement (chapitre 10 : cible source, cible voisine, cible inaccessible, deux chemins ex æquo). - ( ) Sur le graphe du déroulé du cours, ajouter l'arc de poids : refaire le déroulé — l'ordre des figés change-t-il ? le chemin vers ?
- () Vérifier expérimentalement Dijkstra contre la force brute : sur des graphes aléatoires à sommets, comparer aux plus courts chemins obtenus par énumération de tous les chemins simples (chapitre 6) — le banc du chapitre 7, version pondérée.
- () Mesurer le coût : chronométrer Dijkstra naïf sur des graphes aléatoires de à sommets, tracer en log-log, lire la pente.
B. Variantes et pièges
- () Poids nuls : Dijkstra reste-t-il correct si certains poids valent (mais aucun négatif) ? Relire la preuve et trancher.
- ( ) Construire un graphe à poids négatifs sans cycle négatif où Dijkstra échoue, et un autre où, par chance, il réussit — la précondition est nécessaire à la garantie, pas à chaque instance.
- () Le chemin le plus sûr : chaque arc porte une probabilité de bon fonctionnement ; maximiser le produit des probabilités. Adapter Dijkstra (extraction du maximum, relâchement par produit) — ou se ramener à la somme par : montrer l'équivalence des deux approches.
- () Itinéraire avec péages : poids minutes, mais chaque passage par un sommet « péage » ajoute . Modéliser sans changer l'algorithme (reporter le coût des sommets sur les arcs entrants — le dédoublement du chapitre 12 en renfort si besoin).
- () Le second plus court chemin : proposer une méthode simple (pour chaque arc du plus court chemin, le retirer et relancer Dijkstra ; garder le meilleur) — coût, correction, et contre-exemple montrant qu'on ne peut pas se contenter de retirer un seul arc choisi au hasard.
C. A* et heuristiques
- () Vérifier que l'heuristique de Manhattan est admissible sur la grille à pas unitaires, et qu'elle cesse de l'être si l'on autorise les diagonales à coût (proposer alors l'heuristique de Tchebychev ).
- ( ) Reproduire le match de l'exercice résolu 8 sur un labyrinthe à d'obstacles : compter les figés des deux algorithmes sur labyrinthes aléatoires, et tracer la distribution du gain.
- () Visualiser : afficher (matplotlib, chapitre 4) la carte des sommets figés par Dijkstra et par A* sur la même grille — le disque et le fuseau, en image.
- () Le taquin (jeu de pousse-cases) : états configurations, arcs coups ; résoudre par A* avec l'heuristique « somme des distances de Manhattan de chaque case à sa place » (admissible — pourquoi ?) et comparer au BFS pur en nombre d'états explorés.
D. Études de synthèse
- () Le réseau de bus d'une petite ville (à inventer : arrêts, lignes, correspondances) : modéliser, calculer la table des temps depuis la gare, identifier l'arrêt le plus mal desservi (excentricité pondérée).
- ( ) « Tous vers l'hôpital » sur graphe orienté : implémenter la solution du graphe inverse (exercice résolu 10b) et la valider contre exécutions directes sur un petit graphe.
- () La carte routière simplifiée de la France ( villes, distances réelles) : Dijkstra avec arrêt, A* au vol d'oiseau (coordonnées des villes), comparaison des sommets figés sur cinq trajets — le mini-GPS de fin d'année.
- ( ) Dossier final du semestre : pour le problème « évacuation d'un bâtiment » (plan en grille, plusieurs sorties, certaines portes lentes), livrer la chaîne complète — modélisation (graphe implicite pondéré, sortie virtuelle unique reliée à toutes les vraies sorties par des arcs de poids nul : pourquoi est-ce correct ?), algorithme justifié, assertions, jeu de tests par partitionnement, mesure des coûts, et rapport selon les six compétences du programme — la synthèse des chapitres 10 à 14 en un seul livrable.