1) Les cinq routes ont pour longueurs
2,
4,
5,
8 et
4 : elles sont toutes strictement positives, donc l'hypothèse « tous les poids sont positifs ou nuls » du théorème de Dijkstra est remplie, et l'algorithme s'applique.
Le tas contient au départ le seul couple
(0,A).
On extrait
(0,A) :
A devient définitif avec la valeur
0, et l'on relâche ses trois arcs, ce qui empile
(2,B),
(4,C) et
(5,D).
On extrait
(2,B), de première composante minimale :
B devient définitif avec la valeur
2, et l'arc vers
E empile
(10,E).
On extrait
(4,C) :
C devient définitif avec la valeur
4, et l'arc vers
E donne
4+4=8, qui améliore la valeur provisoire
10 : on empile
(8,E).
On extrait
(5,D) :
D devient définitif avec la valeur
5, et il n'a aucun successeur.
On extrait
(8,E) :
E devient définitif avec la valeur
8.
On extrait
(10,E) : le village
E est déjà définitif, le garde-fou l'écarte. Le tas est vide, la boucle s'arrête.
Une variante instrumentée de l'algorithme du cours, qui affiche chaque couple extrait et les couples empilés à sa suite, confirme ce déroulement et livre les deux dictionnaires :
from heapq import heappush, heappop
def dijkstra_trace(successeurs, depart):
"""Renvoie le couple (dist, pere) de l'algorithme de Dijkstra en affichant,
à chaque extraction, le couple sorti du tas et les couples empilés à sa
suite. Précondition : tous les poids sont positifs ou nuls."""
dist = {}
pere = {depart: None}
provisoire = {depart: 0}
tas = [(0, depart)]
while tas:
(d, sommet) = heappop(tas)
if sommet in dist:
print("on extrait", (d, sommet), ": déjà définitif, on passe")
continue
dist[sommet] = d
empiles = []
for (voisin, poids) in successeurs[sommet]:
if voisin in dist:
continue
if voisin not in provisoire or d + poids < provisoire[voisin]:
provisoire[voisin] = d + poids
pere[voisin] = sommet
heappush(tas, (d + poids, voisin))
empiles.append((d + poids, voisin))
print("on extrait", (d, sommet), ": on empile", empiles)
return dist, pere
routes = {"A": [("B", 2), ("C", 4), ("D", 5)], "B": [("E", 8)],
"C": [("E", 4)], "D": [], "E": []}
dist, pere = dijkstra_trace(routes, "A")
print(dist)
print(pere)
Sortie obtenue :
on extrait (0, 'A') : on empile [(2, 'B'), (4, 'C'), (5, 'D')]
on extrait (2, 'B') : on empile [(10, 'E')]
on extrait (4, 'C') : on empile [(8, 'E')]
on extrait (5, 'D') : on empile []
on extrait (8, 'E') : on empile []
on extrait (10, 'E') : déjà définitif, on passe
{'A': 0, 'B': 2, 'C': 4, 'D': 5, 'E': 8}
{'A': None, 'B': 'A', 'C': 'A', 'D': 'A', 'E': 'C'}
En remontant les pères depuis
E, on obtient le trajet
A,
C,
E, de longueur
4+4=8, ce qui est bien la valeur trouvée. Le trajet
A,
B,
E emprunte lui aussi deux routes, mais il coûte
2+8=10 : un parcours en largeur, qui minimise le nombre de routes, n'aurait pas tranché entre les deux. Cinq villages deviennent définitifs, c'est-à-dire tous, et six couples ont été extraits, dont un périmé.
2) La fonction traduit la définition : la valeur à l'arrivée est nulle, l'heuristique est positive, et chaque arc vérifie l'inégalité triangulaire.
def est_consistante(successeurs, h, arrivee):
"""Renvoie True si l'heuristique h, donnée par un dictionnaire, est
consistante sur le graphe pondéré successeurs pour l'arrivée arrivee :
h[arrivee] vaut 0, h est positive, et tout arc (u, v) de poids p vérifie
h[u] <= p + h[v]. Complexité : O(n + m)."""
if h[arrivee] != 0:
return False
for sommet in successeurs:
if h[sommet] < 0:
return False
for (voisin, poids) in successeurs[sommet]:
if h[sommet] > poids + h[voisin]:
return False
return True
coord = {"A": (0, 0), "B": (2, 0), "C": (3, 1), "D": (0, 5), "E": (6, 0)}
routes = {"A": [("B", 2), ("C", 4), ("D", 5)], "B": [("E", 8)],
"C": [("E", 4)], "D": [], "E": []}
def manhattan(u, v):
return abs(coord[u][0] - coord[v][0]) + abs(coord[u][1] - coord[v][1])
h = {village: manhattan(village, "E") for village in coord}
print(h)
print(est_consistante(routes, h, "E"))
print(est_consistante(routes, {v: 2 * h[v] for v in h}, "E"))
Sortie obtenue :
{'A': 6, 'B': 4, 'C': 4, 'D': 11, 'E': 0}
True
False
L'heuristique doublée échoue dès le premier arc : elle estime
12 le trajet restant depuis
A, alors que franchir la route vers
B, de longueur
2, laisse une estimation de
8, et
12>2+8.
Démonstration générale. Notons
μ(u,v)=∣xu−xv∣+∣yu−yv∣ la distance de Manhattan, et
t l'arrivée, de sorte que
h(u)=μ(u,t).
D'abord
h(t)=μ(t,t)=0, et
h est positive comme somme de deux valeurs absolues.
Soit ensuite
(u,v) une route de longueur
p. La distance de Manhattan vérifie l'inégalité triangulaire : pour trois points quelconques,
μ(u,t)≤μ(u,v)+μ(v,t), car cette inégalité vaut séparément sur chaque coordonnée,
∣xu−xt∣≤∣xu−xv∣+∣xv−xt∣ et de même pour les ordonnées, et il suffit de les ajouter.
Par hypothèse sur le réseau,
μ(u,v)≤p. Donc
h(u)=μ(u,t)≤μ(u,v)+μ(v,t)≤p+h(v).
L'heuristique
h est donc consistante, et par conséquent admissible. C'est exactement l'hypothèse que demande le théorème de A étoile, et elle tient à la seule géométrie du réseau, sans aucun calcul de distance.
3) La priorité d'un village n'est plus sa distance provisoire
g au départ, mais la somme
f=g+h.
On part du triplet
(6,0,A).
On extrait
(6,0,A) :
A devient définitif, et l'on empile
(6,2,B), car
2+4=6, puis
(8,4,C), car
4+4=8, puis
(16,5,D), car
5+11=16.
On extrait
(6,2,B) :
B devient définitif avec
g=2, et l'arc vers
E empile
(10,10,E).
On extrait
(8,4,C) :
C devient définitif avec
g=4, et l'arc vers
E donne
g=8, qui améliore
10 : on empile
(8,8,E).
On extrait
(8,8,E) : l'arrivée est extraite, l'algorithme s'arrête et renvoie
8.
La même instrumentation, portée sur A étoile, confirme ces quatre extractions :
def a_etoile_trace(successeurs, depart, arrivee, h):
"""Déroule A étoile en affichant chaque triplet extrait et chaque triplet
empilé, les triplets s'écrivant (f, g, sommet).
Précondition : h est consistante et les poids sont positifs ou nuls."""
dist = {}
provisoire = {depart: 0}
tas = [(h[depart], 0, depart)]
while tas:
triplet = heappop(tas)
(_, d, sommet) = triplet
if sommet in dist:
continue
dist[sommet] = d
print("on extrait", triplet)
if sommet == arrivee:
return d, len(dist)
for (voisin, poids) in successeurs[sommet]:
if voisin in dist:
continue
if voisin not in provisoire or d + poids < provisoire[voisin]:
provisoire[voisin] = d + poids
empile = (d + poids + h[voisin], d + poids, voisin)
heappush(tas, empile)
print(" on empile", empile)
return None, len(dist)
a_etoile_trace(routes, "A", "E", h)
Sortie obtenue :
on extrait (6, 0, 'A')
on empile (6, 2, 'B')
on empile (8, 4, 'C')
on empile (16, 5, 'D')
on extrait (6, 2, 'B')
on empile (10, 10, 'E')
on extrait (8, 4, 'C')
on empile (8, 8, 'E')
on extrait (8, 8, 'E')
Quatre villages sont rendus définitifs, contre cinq pour Dijkstra, et la distance trouvée est la même,
8 : A étoile ne change pas le résultat, seulement le chemin de calcul.
Le village évité est
D. Il est pourtant proche du départ, à la distance
5, et Dijkstra l'avait rendu définitif avant
E. Mais il est situé à l'opposé de l'arrivée : son estimation
h(D)=11 lui donne la priorité
5+11=16, qui reste au fond du tas pendant que les villages menant vers
E en sortent. La différence se lit bien ainsi : Dijkstra ordonne les villages par leur éloignement du départ, A étoile par la longueur estimée du trajet complet.
4) Le village
E est découvert pour la première fois lors de l'extraction de
B, avec la valeur provisoire
g=10, et le triplet
(10,10,E) est alors empilé. Il n'est extrait que plus tard, avec la valeur
g=8, celle qu'a empilée l'extraction de
C.
Un programme qui s'arrêterait à la découverte de l'arrivée renverrait donc
10, au lieu de
8 : il se tromperait de
2 sur un trajet de
8. Une valeur provisoire n'est qu'une borne supérieure de la distance, et c'est l'extraction du tas, et elle seule, qui la rend définitive ; entre la découverte et l'extraction, un trajet meilleur peut encore arriver, et ici il arrive.
À retenir : on vérifie la positivité des poids avant d'invoquer Dijkstra, la consistance de l'heuristique avant d'invoquer A étoile, on mesure le gain en comptant les sommets rendus définitifs contre l'heuristique nulle, et l'on s'arrête à l'extraction de l'arrivée, jamais à sa découverte.