• Aucun résultat trouvé

4.2 L’algorithme de Dijkstra

4.2.2 Implémentation et complexité

File de priorité. Généralement on implémente l’ensemble Q par une file de priorité (priority queueen Anglais). C’est une structure de données qui permet de gérer certaines opérations sur les ensembles et qui sont les suivantes8 :

• créer une file vide ;

• d’ajouter à la file un élément et sa priorité ;

• d’extraire de la file l’élément de plus haute priorité ; et

Pour être plus précis, unecléc est associée à chaque élémentvpermettant de détermi-ner la priorité de l’élément. Pour notre utilisation, l’élément de plus haute priorité est celui avec la plus petite clé. C’est donc le couple (v, c) qui est inséré dans la file. Pour Dijkstra, la clé est c = co ˆut[v] si bien que l’élément de plus haute priorité est celui de coût minimum.

Des variantes plus sophistiquées de file de priorité permettent en plus d’augmenter la priorité d’un élément déjà dans la file en diminuant (=décrémenter) sa clé. C’est malheureusement plus complexe à programmer car il faut gérer, à chaque mise à jour de la file, la position de chaque élément dans la file.

DansDijkstraon remarque que l’on :

• parcourt chaque arc au plus une fois, ce qui couteO(m) ;

• extrait deQau plus une fois chacun des sommets, ce qui couteO(n·tmin(n)) ;

• ajoute au plus chacun des sommets àQ, ce qui couteO(n·tadd(n)) ; et

• modifie les coûts au plus autant de fois qu’il y a d’arcs, ce qui couteO(m·tdec(n)).

Ici tmin(n),tadd(n),tdec(n) sont respectivement les complexités en temps des opéra-tions d’extraction du minimum, d’ajout et de décrémentation de la clé d’un élément d’une file de taille au plusn.

Les sommets deP se gèrent par un simple marquage qui coute au total un temps et un espace enO(n). Au total la complexité en temps deDijkstraest donc

O(m+n·tmin(n) +n·tadd(n) +m·tdec(n)).

Il faudrait ajouter le temps de création (voir de suppression) d’une file vide qui sont des opérations qui s’implémentent facilement en O(1). Notons que les différentes tables, y compris la file, ne contiennent que des sommets distincts (avec leurs clés) et donc occupent un espaceO(n).

Parenthèse.On pourra se référer àWikipédiapour plus de détails et les diverses implémen-tations possibles, ainsi que et leurs complexités, des files de priorités. On notera qu’il existe une réduction des files de priorité aux algorithmes de tri. Plus précisément, s’il est possible de triernclés en tempsSort(n), alors il existe une file de priorité supportant l’insertion et la 8. On pourrait rajouter, mais c’est pas essentiel, la suppression d’une file (précédemment créée) et de tester si une file est vide (qui est implicite dans l’opération d’extraction).

144 CHAPITRE 4. NAVIGATION suppression de l’élément de plus haute priorité en tempsO(Sort(n)/n). Ce résultat de

est du à Thorup [Tho07], le même chercheur en informatique qui a produit l’algorithme de tri le plus rapide connu (qui n’est pas par comparaisons) ainsi qu’un algorithme de calcul des plus courts chemins d’une complexité meilleure que celle de Dijkstra. Cela sera redis-cuté dans la parenthèse de la page 146. À partir des meilleures complexités connues pour Sort(n), on en déduit des complexités enO(log logn)(et mêmeO(p

log logn)en moyenne) pour les opérations sur les files de priorité.

Mise à jour paresseuse. En fait on peut se passer d’implémenter la décrémentation de clé si on est pas trop limité en espace. Au lieu d’essayer de décrémenter la clé c en c0 < c d’un élément v déjà dans la file, on peut simplement faire une mise à jour de manière paresseuse : on ajoute à la file un nouveau couple (v, c0) (cf. figure 4.9). Cela n’a pas de conséquences dans la mesure où l’on extrait à chaque fois l’élément de clé minimum. C’est donc (v, c0), et sa dernière mise à jour, que l’on traitera en premier. Il en va de même en fait pour toute modification de clé, qu’elle soit une incrémentation ou décrémentation. Si plus tard on souhaite augmenterc0 enc00 > c0, alors on ajoute (v, c00) à la file. Dans tous les cas, c’est la valeur minimum qui sera extraite en premier.

Q

(v, c) (v, c0)

(v, c00)

Figure4.9 – Mise à jour paresseuse des clés d’une file de priorité Q, ici im-plémentée par un tas minimum. C’est la copie devavec la plus petite clé (ici c0) qui sera extraite en premier. Dans le cas d’un tas binaire, la silhouette du tas devrait être plus ramassée, car en réalité chaque couche est deux fois plus grande que celle juste au-dessus.

Pour mettre en œuvre cette mise à jour paresseuse, il faut réorganiser les instructions en2dcorrespondant à la mise à jour des coûts des voisinsv deu :

ii. Siv<Q, ajouterv àQ

iii. Sinon, sic>co ˆut[v] continuer la boucle iv. co ˆut[v] :=c, parent[v] :=u

7→ ii. co ˆut[v] :=c, parent[v] :=u iii. ajouter9 vàQ

9. En fait, ici c’est (v,co ˆut[v],parent[v]) qu’on ajoute à Q, c’est-à-dire v et toutes ses informations associées (dont le coût). En pratique c’est unestructreprenant toutes ces informations qui est ajoutée à Q.

4.2. L’ALGORITHME DE DIJKSTRA 145 Notez qu’au passage le code se simplifie (vive la paresse !) et surtout évite le test

«v<Q» qui n’est pas adapté aux files.

Cependant on crée, par cet ajout inconditionnel, le problème que les copies de v(le couple initial (v, c) puis (v, c00)) vont plus tard être extraites de la file. Cela n’était pas possible auparavant, mais c’est inexorable maintenant à cause du Tant queQ,∅. Dans Dijkstraon peut résoudre ce problème grâce à l’ensembleP, puisqu’une fois extrait, un sommet se retrouve dansP et n’a plus à être traité de nouveau.

Il suffit donc de modifier l’instruction2cdeDijkstraainsi : (c) SiuP, continuer la boucle, sinon l’ajouter àP

qui remplace l’ajout simple deuàP en2c. Dans continuer la boucle il faut comprendre revenir au début de l’instruction 2 du Tant queQ,∅, ce qui en C se traduit par un simplecontinue.

L’autre inconvénient de cet ajout systématique est qu’on peut être amené à ajouter plus denéléments à la file. Mais cela est au plusO(m) car le nombre total de modifica-tions, on l’a vu, est au plus le nombre d’arcs qui vaut 2m. L’espace peut donc grimper à O(m). Mais, on va le voir, cela n’affecte pas vraiment la complexité en temps10qui vaut donc :

O(m+m·(tmin(m) +tadd(m))).

Implémentation par tas. Une façon simple d’implémenter une file de priorité est d’utiliser un tas (heap en Anglais). Avec un tas classique implémenté par un arbre binaire quasi-complet (qui est lui-même un simple tableau), on obtient11 tmin(m) = O(logm) = O(logn) et tadd(m) = O(logm) = O(logn) [Question. Pourquoi O(logm) = O(logn) ?].[Exercice. Si pour le tas on utilise un arbre b-aire au lieu d’un arbre binaire (b= 2), que deviennent les complexités pour l’ajout et la suppression du minimum en nombre de comparaisons d’éléments ?]Ce qui donne finalement, pourDijkstraavec im-plémentation par tas binaire et mise à jour paresseuse, une complexité de :

O(m·logn). (4.2)

Cependant, il existe des structures de données pour les tas qui sont plus sophisti-quées (voir la parenthèse de la page143), notamment le tas de Fibonacci. Il permet un temps moyen par opérations – on parle aussi decomplexité amortie– plus faible que le tas binaire. Il existe même une version, appelée tas de Fibonacci strict [SBLT12], avec tdec(n) =tadd(n) = O(1) et tmin(n) = O(logn) dans le pire des cas et pas seulement en moyenne. La complexité finale tombent alors à O(m+nlogn). On peut montrer que

10. En plus lesnavigations meshesà base de triangulations du plan possèdentm <3narêtes[Question.

Pourquoi ?]. Et puis il faut partir gagnant (surtout vrai avecA*) : on espère bien évidemment trouver la cibletavant d’avoir parcouru lesmarcs du graphe !

11. C’est la suppression du minimum qui couteO(logm). Le trouver à proprement parler est enO(1).

146 CHAPITRE 4. NAVIGATION c’est la meilleure complexité que l’on puisse espérer pour Dijkstra. Mais ce n’est pas forcément le meilleur algorithme pour le calcul des distances dans un graphe !

Parenthèse.Le principe consistant à prendre à chaque fois le sommet le plus proche implique que dansDijkstrales sommets sont parcourus dans l’ordre croissant de leur distance depuis la sources. Si, comme dans la figure4.10, la sourcespossèden−1voisins, le parcours de ses voisins selon l’algorithme donnera l’ordre croissant des poids de ses arêtes incidentes. En effet, l’unique plus court chemin entreset vi est précisément l’arêtesvi. Ceci implique

W

une complexité d’au moinsΩ(nlogn)pour Dijkstra, car il faut se souvenir que triern0 = n−1nombres nécessitent au moinslog2(n0!) =n0log2n0−Θ(n0) =Ω(nlogn)comparaisons.

D’un autre coté la complexité est au moins le nombre total d’arêtes, m. Car, si toutes les arêtes et leurs poids ne sont pas examinés, l’algorithme pourrait se tromper. Il suit que la complexité deDijkstraest au moins12

max{m, nlogn} > 1

2(m+nlogn) = Ω(m+nlogn).

En utilisant une structure de données adéquate (notamment un tas de Fibonacci), Dijks-trapeut effectivement être implémenté pour atteindre la complexité deO(m+nlogn). Ce-pendant, on ne peut pas en déduire queDijkstraest l’algorithme ayant la meilleure com-plexité permettant de calculer les distances à partir d’une source donnée. Car rien n’indique que le principe du sommet le plus proche soit le meilleur. En fait, un algorithme de com-plexité optimaleO(n+m)[Question. Pourquoi est-ce optimal ?]a été trouvé par [Tho99].

Bien sûr, cet algorithme ne parcourt pas les sommets par ordre croissant des distances de-puiss.

12. Attention ! Il y a ici deux arguments différents menant à deux bornes inférieures sur la complexité en temps. Schématiquement, l’un dit qu’il faut au moins 1h, tant dit que l’autre dit qu’il en faut au moins 2h. On ne peut pas conclure directement qu’il y a une situation où l’algorithme doit pendre 3h.

On peut seulement en déduire qu’il faut au moins le maximum des deux bornes inférieures. Rien ne dit, par exemple, qu’on ne peut pas commencer à trier lesnpoids pendant qu’on examine lesmarêtes.

Cependant, max{x, y}=Ω(x+y)[Question. Pourquoi ?].