Bonjour,
Dans notre leçon précédente, nous avons conclu notre étude des parcours de graphes en nous penchant sur la détection des composantes fortement connexes. Ces algorithmes, comme ceux de Kosaraju et Tarjan, répondent à la question de l'accessibilité : quels sommets peuvent atteindre quels autres ?
Aujourd'hui, nous introduisons une dimension quantitative à nos graphes : les poids. Les arêtes (ou arcs) ont désormais un coût, une distance ou un temps associé. Notre question fondamentale évolue : quel est le chemin le moins coûteux d'un point à un autre ?
Cette leçon est consacrée à l'un des algorithmes les plus célèbres et fondamentaux pour répondre à cette question : l'algorithme de Dijkstra. L'objectif est non seulement de comprendre sa logique, mais aussi de l'implémenter efficacement en Python en utilisant une structure de données essentielle pour sa performance : la file de priorité.
1. Le Problème du Plus Court Chemin et l'Approche Gloutonne
Le problème que nous abordons est celui du plus court chemin à origine unique (Single-Source Shortest Path, SSSP).
Formellement, étant donné un graphe orienté et pondéré , une fonction de poids qui associe un poids non-négatif à chaque arc, et un sommet source , l'objectif est de trouver, pour chaque sommet , la longueur du plus court chemin de à . Nous notons cette longueur .
Pour ce faire, l'algorithme de Dijkstra maintient pour chaque sommet :
d[v]: une estimation (un majorant) de . Initialement,d[s] = 0etd[v] = ∞pour tout .π[v]: le prédécesseur de dans le chemin desàvactuellement connu comme étant le plus court.
L'opération fondamentale de l'algorithme est le relâchement (relaxation). Pour un arc de poids , relâcher cet arc consiste à vérifier si passer par offre un meilleur chemin vers que celui connu jusqu'à présent.
Relâcher(u, v, w):
Si d[v] > d[u] + w(u, v) alors
d[v] ← d[u] + w(u, v)
π[v] ← u
L'algorithme de Dijkstra est un algorithme glouton. À chaque étape, il sélectionne le sommet non encore "finalisé" qui a la plus petite estimation de distance d[u], considère que cette estimation est désormais la distance définitive, et relâche tous les arcs qui partent de .
Le document suivant, issu d'une préparation à l'Agrégation, formalise ces notions.
[PDF] Préparation agrégation : Algorithmique
Ce document formalise le problème des plus courts chemins et introduit la procédure de relâchement. C'est le langage rigoureux que vous devez maîtriser.
Lisez la section 2.5 "Algorithmes de plus courts chemins" jusqu'à la fin de la description de la procédure "Relâcher(u, v, w)". Concentrez-vous sur la définition formelle de la longueur d'un chemin, de δ(u, v), et de la sous-structure optimale.
2. Mécanique de l'Algorithme de Dijkstra
L'algorithme de Dijkstra maintient un ensemble de sommets dont la distance la plus courte depuis la source a été finalisée. À chaque étape :
- Il extrait le sommet
uqui n'est pas encore finalisé et qui possède la plus petite valeurd[u]. - Il ajoute
uà l'ensemble des sommets finalisés. - Pour chaque voisin
vdeu, il effectue l'opération de relâchementRelâcher(u, v, w).
Ce processus continue jusqu'à ce que tous les sommets accessibles aient été finalisés. La vidéo suivante illustre ce mécanisme de manière très claire, étape par étape.
Dijkstra's Algorithm - A step by step analysis, with sample Python code
Cette vidéo de Glassbyte offre une excellente animation visuelle du déroulement de l'algorithme. Elle permet de construire une intuition solide du processus avant de passer au code.
Regardez la section allant de 05:09 à 12:03. Observez attentivement : L'initialisation des distances (0 pour la source, ∞ pour les autres). Comment, à chaque étape, on choisit le sommet avec la plus petite distance courante. Le processus de mise à jour des distances des voisins (la relaxation). La distinction entre un sommet simplement atteint et un sommet "visité" (finalisé).
La question cruciale pour une implémentation efficace est : comment trouver rapidement, à chaque étape, le sommet non finalisé avec la plus petite distance ? Un balayage de tous les sommets serait en , menant à une complexité totale de . Nous pouvons faire mieux.
3. Le Rôle Central de la File de Priorité
C'est ici qu'intervient la file de priorité (priority queue), généralement implémentée par un tas min (min-heap). Cette structure de données est conçue pour stocker des éléments et en extraire efficacement celui qui a la plus petite "priorité" (ici, la plus petite distance d[u]).
L'algorithme s'articule ainsi :
- Initialiser les distances et les prédécesseurs.
- Insérer
(0, s)dans une file de priorité min (le couple(distance, sommet)). - Tant que la file de priorité n'est pas vide :
a. Extraire le couple(dist, u)avec la plus petite distance.
b. Siua déjà été finalisé, l'ignorer et continuer.
c. Marquerucomme finalisé.
d. Pour chaque voisinvdeu, relâcher l'arc(u,v). Si la distanced[v]est améliorée, insérer(d[v], v)dans la file de priorité.
La vidéo suivante explique très bien l'intérêt de cette structure.
Dijkstra's Algorithm - A step by step analysis, with sample Python code
Nous retournons à la vidéo précédente pour nous concentrer sur la section qui justifie et explique la file de priorité.
Regardez la section de 12:03 à 13:31. Elle clarifie pourquoi une simple liste ne suffit pas et comment la file de priorité assure que l'on traite toujours le sommet le plus prometteur.
Le pseudo-code ci-dessous formalise cette approche.

Pour tester votre compréhension du fonctionnement de la file de priorité, considérez la question suivante :

Réflexion : Au départ, la file contient (0, 0). Après avoir traité le sommet 0, on ajoute ses trois voisins (1, 4, 7). La file contient 3 éléments. Après avoir traité le sommet 4 (le plus proche), on ajoute son voisin 5... Continuez le raisonnement. Le nombre maximal d'éléments dans la file sera 4.
4. Implémentation en Python avec heapq
Passons à la mise en œuvre. En Python, le module heapq fournit une implémentation efficace des tas. Il est essentiel de le maîtriser.
L'article ci-dessous est une ressource exceptionnelle. Il fournit une implémentation Python propre et idiomatique, l'explique ligne par ligne, et analyse sa complexité.
Dijkstra's Algorithm with a Priority Queue - Code and Debug
Cet article de Code and Debug est un guide complet pour l'implémentation de Dijkstra avec une file de priorité en Python. Il sera votre référence pour le codage.
Étudiez les sections 3, 4 et 5 de cet article. Section 3 "Deep intuition & approach": Assurez-vous de comprendre les concepts clés, en particulier la "Visited / relaxed rule" et "Edge relaxation". Section 4 "Python code": Analysez attentivement le code. Notez l'utilisation de heapq.heappush et heapq.heappop. Repérez la gestion des "stale entries" (entrées obsolètes dans le tas), un détail d'implémentation important. Section 5 "Step-by-step code explanation": Lisez cette section pour valider votre compréhension de chaque ligne du code.
Voici le cœur de l'implémentation, inspiré de la ressource :
import heapq
def dijkstra(graph, start_node):
# graph: dictionnaire {node: [(neighbor, weight), ...]}
# 1. Initialisation
distances = {node: float('inf') for node in graph}
distances[start_node] = 0
# File de priorité : stocke des tuples (distance, node)
priority_queue = [(0, start_node)]
# 2. Boucle principale
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
# Ignorer si on a déjà trouvé un chemin plus court
if current_distance > distances[current_node]:
continue
# 3. Relaxation des voisins
for neighbor, weight in graph.get(current_node, []):
distance = current_distance + weight
# Si un meilleur chemin est trouvé
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
5. Complexité et Limites
-
Complexité temporelle : Avec une file de priorité implémentée par un tas binaire, chaque insertion (
heappush) et extraction du minimum (heappop) prend un temps en . Chaque sommet est extrait une seule fois. Chaque arc est relâché une seule fois. La complexité est donc dominée par les opérations sur le tas, menant à . C'est une amélioration significative par rapport à pour les graphes épars (où est proche de ). -
Complexité spatiale : Elle est de pour stocker le graphe, le tableau des distances et la file de priorité (qui peut contenir au pire tous les sommets).
-
Limite fondamentale : L'algorithme de Dijkstra ne fonctionne que si tous les poids des arcs sont non-négatifs. Le caractère glouton de l'algorithme (finaliser la distance d'un sommet de façon définitive) est mis en échec par les poids négatifs, qui pourraient révéler un chemin plus court via un sommet déjà finalisé.
Shortest Path Algorithms Explained (Dijkstra's & Bellman-Ford)
Pour comprendre cette limitation cruciale, cette vidéo de b001 illustre parfaitement pourquoi Dijkstra échoue en présence de poids négatifs.
Regardez la section de 05:24 à 07:15. L'exemple est très parlant et montre comment un arc à poids négatif peut invalider une décision gloutonne prise antérieurement.
Conclusion
L'algorithme de Dijkstra est une pierre angulaire de l'algorithmique des graphes, et son implémentation efficace est un savoir-faire indispensable pour l'Agrégation.
Points clés à retenir :
- Dijkstra résout le problème des plus courts chemins depuis une source unique dans un graphe à poids non-négatifs.
- Son approche est gloutonne : il finalise à chaque étape le sommet le plus proche non encore finalisé.
- L'utilisation d'une file de priorité (tas min) est essentielle pour une implémentation efficace, menant à une complexité de .
- L'opération de relâchement est au cœur du mécanisme de mise à jour des distances.
Dans notre prochaine leçon, nous nous attaquerons à la preuve de correction de l'algorithme de Dijkstra. C'est une démonstration classique, souvent exigée, qui repose sur un raisonnement par récurrence et qui justifie formellement pourquoi l'approche gloutonne fonctionne dans le cas des poids non-négatifs.