Algorithme de Dijsktra

publicité
Université du Québec
École de technologie supérieure
Département de génie de la production automatisée
Auteur : Tony Wong Ph.D., ing.
Département de génie de production automatisée
École de technologie supérieure
Courriel : wong.wong@ etsmtl.ca
Algorithme de Dijsktra
Chemins minimaux à partir d’une source
Section
1
Algorithme de Dijsktra
Les chemins minimaux à partir d’une seule source
Présentation
À partir d’un graphe, l’algorithme de Dijsktra utilise le parcours en « largeur d’abord »
et l’approche « gourmande » (greedy) pour trouver les chemins les plus courts entre une
source et toutes les destinations du graphe.
Puisqu’un graphe est composé de nœuds et d’arêtes, l’algorithme de Dijsktra peut
trouver les chemins les plus courts liant un nœud quelconque à tous les autres noeuds
du graphe en une seule exécution.
Conditions d’applicabilité
L’algorithme de Dijsktra s’opère dans l’univers des graphes. Les graphes acceptés
peuvent être orientés ou non. La figure 1a) montre un graphe non orienté alors que la
figure 1b) montre un graphe orienté.
1a)
1b)
• Figure 1 Graphe non orienté (a), graphe orienté (b).
Un graphe peut également être pondéré. Dans ce cas, la valeur de pondération est
indiquée sur les arêtes du graphe. La figure 2 montre un graphe orienté et pondéré.
2
700
5
600
6
700
300
700
4
3
1700
400
700
1
7
1200
8
• Figure 2 Graphe orienté et pondéré.
De cette façon, nous pouvons représenter un nombre de situations réelles par des
graphes. Par exemple, le trafic aérien où les nœuds sont des villes et les pondérations
sont le coût des billets d’embarquement reliant deux villes. Dans le domaine de la
production industrielle, les nœuds peuvent représenter différentes étapes d’un
processus de fabrication alors que les pondérations peuvent représenter le temps
requis pour passer d’une étape à l’autre.
L’application de l’algorithme de Dijsktra exige un graphe dont les caractéristiques
sont :
Orienté ou non orienté.
Pondéré ou non pondéré.
Valeur de la pondération non négative (c’est à dire, ≥ 0).
Enfin, il doit exister au moins un chemin partant du nœud de départ vers tous les
autres nœuds du graphe. Évidemment, cette condition ne s’applique pas pour le
nœud de départ.
Il est donc important de s’assurer que les valeurs inscrites sur les arêtes des
graphes soient non négatives.
Nature des graphes
Cette section est une brève introduction, pour les non initiés, de la théorie des
graphes. Les lecteurs qui ne s’intéressent qu’aux applications de l’algorithme de
Dijkstra sont priés de passer à la section suivante.
Un graphe G est représenté par la notation
3
G = (N, A)
(1)
où N est l’ensemble des nœuds et A est l’ensemble des arêtes et chaque arête est un
pair (u, v) où u, v ∈ N. Si l’ordre des nœuds dans le pair (u, v) est important alors le
graphe est orienté. Dans le cas contraire, le graphe est non orienté.
Dans un graphe orienté, on dit qu’un nœud v est adjacent au nœud u si et seulement si
le pair (u, v) ∈ A. C’est-à-dire, l’arête (u, v) existe dans le graphe. Pour exprimer la
même notion dans un graphe non orienté, on dit qu’un nœud v est adjacent au nœud
u (et par le fait même u est adjacent à v) si (u, v) ∈ A et (v, u) ∈ A.
Lorsqu’un graphe est pondéré, on ajoute un troisième composant dans l’équation (1).
C’est-à-dire,
G = (N, A, c)
(2)
où c : N × N → ℜ+ est une fonction donnant le coût de l’arête reliant deux nœuds. Le
domaine de la fonction c(· ) est l’ensemble des nœuds d’où le produit cartésien N × N.
Rappelons que le coût doit être non négative pour permettre l’application de
l’algorithme de Dijsktra, c’est pour cette raison que l’image de la fonction est ℜ+.
Un chemin dans un graphe est une séquence de nœuds u1, u2, …, uW tel que (ui , ui+1)
∈ A pour 1 ≤ i ≤ W – 1. La longueur d’un chemin est alors simplement W – 1.
Également, un nœud u peut avoir un chemin vers lui-même. Si ce chemin n’a pas
d’arêtes alors la longueur de ce chemin est nulle (zéro). Par contre, si ce chemin
possède une arête (u, u) alors le chemin joignant le nœud u est appelé une boucle et sa
longueur demeure nulle (zéro). Un chemin est un chemin simple si tous les nœuds du
chemin sont distincts excepté le nœud de départ et le nœud d’arrivée.
Soit un nœud de départ u1 et un nœud d’arrivée uW. Dans un graphe orienté, un
chemin de longueur ≥ 1 avec u1 = uW est appelé un cycle. Un cycle est un cycle simple
si le chemin composant le cycle est un chemin simple. Dans un graphe non orienté,
on impose une contrainte supplémentaire dans la formation d’un cycle : il faut que les
nœuds du chemin soient tous des nœuds distincts. La raison de cette contrainte
supplémentaire est évidente si l’on considère que le chemin u1, u2, u1 dans un graphe
non orienté est en fait la même arête. C’est-à-dire (u1, u2) = (u2, u1) pour un graphe non
orienté. Donc, nous avons la nomenclature suivante :
Graphe orienté
Graphe non orienté
Cycle simple
Cycle non simple où les nœuds du
chemin ne sont pas tous distincts
Cycle
N/A
Un graphe orienté sans cycle est un graphe acyclique. Ces graphes ont beaucoup
d’applications dans la pratique. Notamment dans la représentation et l’analyse des
4
programmes parallèles. Nous les avons donnés un nom particulier, le DAG.
L’acronyme DAG signifie tout simplement « Direct Acyclic Graphs ».
S’il existe un chemin reliant tous les nœuds du graphe, ce dernier est appelé un graphe
connecté pour un graphe non orienté. Pour un graphe orienté, il est appelé graphe
fortement connecté. Si on enlève la direction des arêtes d’un graphe orienté et que le
graphe non orienté résultant est connecté alors on appelé le graphe orienté, un graphe
orienté faiblement connecté. Le tableau suivant résume cette propriété des graphes.
Graphe orienté
Fortement connecté
Faiblement connecté – éliminer
l’orientation des arêtes. Le graphe
résultant est connecté.
Graphe non orienté
Connecté
Connecté
Enfin, s’il existe au moins une arête reliant tous les pairs de nœuds dans le graphe, le
graphe est un graphe complet. On peut constater que ce type de graphes peut
représenter un grand nombre de situations. Par exemple, un ordinateur parallèle à
mémoire commune, les chemins de communication entre ordinateurs reliés par un
réseau Ethernet.
Lorsqu’un graphe est pondéré (voir équation 2), nous pouvons associer un coût aux
différents chemins contenus dans le graphe. Ainsi, le coût du chemin u1, u2, …, uW tel
que (ui , ui+1) ∈ A pour 1 ≤ i ≤ W – 1 est donné par
W −1
C = ∑ c(ui , ui +1 ).
(3)
i =1
Un grand nombre de problèmes consistent à trouver un chemin entre u1 et uW et en
même temps minimiser le coût associé.
Dans la théorie des graphes, nous faisons souvent l’abstraction de la façon dont les
graphes sont représentés. En informatique, dont le but consiste à trouver des
solutions aux problèmes mathématiques, la représentation des graphes est une
question primordiale. L’une des représentation la plus utilisée est la liste des nœuds
adjacents. Dans cette représentation, nous dressons une liste de nœuds contenus dans
le graphe et on les lie à d’autres listes énumérant les nœuds adjacents. La figure 3 est
une représentation figurative d’une liste de nœuds adjacents du graphe orienté de la
est utilisé ici pour représenter le vide ou la fin d’une liste.
figure 2. Le symbole
L’avantage de cette représentation réside dans sa simplicité et dans l’économie de son
implantation. En fait, l’espace mémoire nécessaire pour emmagasiner la liste de
nœuds adjacents est proportionnel au nombre des nœuds et au nombre d’arêtes du
graphe (C’est-à-dire, | N | + | A | où |· | est le nombre cardinal de l’ensemble).
5
4
5 7
5
6
6
7
3 8
8
1
400
2
700
4
300
5
600
3
1700
3
6
700
2
3
700
2 4
700
1
7
1200
8
8
• Figure 3 a) Liste des nœuds adjacents. (b) Graphe orienté de la figure 2.
Il est évident que la liste des nœuds adjacents peut avoir des nœuds non distincts pour
les graphes orientés (exemple : dans la figure 3, le nœud 8 apparaît deux fois parmi les
nœuds adjacents). Pour les graphes non orientés, les nœuds de la liste peuvent être
tous distincts.
Il arrive que deux nœuds soient reliés entre eux par plus d’une arête pour indiquer
différentes situations concrètes. Un graphe admet ce genre de liens à condition qu’il
existe des caractéristiques différentes. Dans le cas des graphes orientés et pondérés,
on peut lier deux nœuds par plus d’une arête si la direction ou la pondération est
différente. Par exemple, le graphe de la figure 4 montre un petit quadrilatère des rues
du centre-ville de Montréal. Certaines de ces rues sont de sens unique alors d’autres
sont de double sens de circulation.
700
5
7
600
2 Ste-Catherine - McGill
3 Ste-Catherine - Université
4 Cathcart - Mansfield
6
5 Cathcart - McGill
6 Cathcart - Université
7 René Lévesque - Mansfield
700
300
700
4
1 Ste-Catherine - Mansfield
3
1700
400
2
700
1
1200
8
8 René Lévesque - Université
• Figure 4 représentation particulière d'un graphe orienté.
On constate dans le graphe orienté de la figure 4 que les rues à double sens de
circulation sont simplement représentées par deux arêtes d’orientation opposée. La
figure 5 donne la liste des nœuds adjacents du graphe de la figure 4.
6
4
1 5 7
5
4 6
6
7
5 3 8
4 8
8
7
1
400
2
700
4
300
5
600
3
1700
1 3
2
6
700
2
3
700
2 4
700
1
7
1200
8
• Figure 5 Liste des nœuds adjacents du graphe de la figure 4.
Attention : Certains algorithmes de graphe n’acceptent pas ce genre de
modification. Vous devez alors ajouter des nœuds au graphe de base pour
permettre l’ajout des liens (ou des pondérations) supplémentaires.
Application de l’algorithme de Dijsktra
Cet algorithme accepte un graphe de nature illustré dans les figures 1, 2 et 4. Pour les
besoins de ce document, nous appliquerons l’algorithme de Dijsktra aux graphes de
type illustré dans la figure 4. L’algorithme de Dijsktra nécessite également une table
pour mémoriser les nœuds visités. Cette table peut être réalisée de multiple façon (liste
de priorité, heaps, etc.). La figure 6 donne le contenu de cette table.
Nœud
Traité
Coût
Chemin
• Figure 6 Table utilisée par l'algorithme de Dijkstra pour mémoriser les informations utiles.
La colonne « Nœud » sert à identifier les nœuds du graphe. La colonne « Traité »
indique si le nœud correspondant est traité (ou non). La colonne « Coût » indique le
coût minimal du chemin passant par ce nœud. Enfin, la colonne « Chemin » donne le
nœud prédécesseur du nœud identifié dans la colonne « Nœud ». Par exemple, dans le
graphe de la figure 4, le nœud 3 peut avoir comme nœuds prédécesseurs le nœud 2 ou
le nœud 6.
7
Voici l’algorithme de Dijsktra donné sous forme de pseudo-code. En pratique, cet
algorithme comprend trois parties : i) réglage initial de la table des données; ii)
parcours de la table pour la formation des chemins à coûts minimaux; iii) la fouille des
chemins minimaux.
Définition des paramètres de l’algorithme
typedef int NŒUD; // un nœud
const int MARQUEUR_ARRÊT = -1; // marqueur spécial
LISTE_NŒUD_ADJACENT lna; // liste des nœuds adjacents
typedef struct _TABLE { // Table des données (voir figure 6)
bool traité;
int coût;
NŒUD chemin;
} TABLE
// MAX_NŒUD est le nombre de nœuds dans le graphe
TABLE table[MAX_NŒUD];
Réglage initial de la table
// source est le nœud de départ
void InitTable(NŒUD source, TABLE T[])
{
// construire la liste des nœuds adjacents à partir du
// graphe
Construire(lna);
// Réglage initial de la table
for (i=0; i<MAX_NŒUD; i++) {
T[i].traité = false;
T[i].coût = ∞; // infini
T[i].chemin = MARQUEUR_ARRÊT; // pas de prédécesseur
}
// Le coût pour atteindre le nœud de départ est toujours zéro
// Ici on indique le nœud de départ pour la fouille des chemins
// à coût minimaux
T[source].coût = 0;
}
Parcours de la table
// Cette fonction imprime le chemin à coût minimal à partir
// du nœud de départ vers un nœud quelconque du graphe
// v est le nœud de destination
void ParcoureTable(NŒUD v, Table T[])
{
if (T[v].chemin != MARQUEUR_ARRÊT) {
ParcoureTable(T[v].chemin, T);
cout << " à";
}
printf(" %d : coût du chemin %d", v, T[v].coût);
}
8
Identification des chemins minimaux
void Dijkstra(LISTE_NŒUD_ADJACENT lna, TABLE T[])
{
1 NŒUD u, v;
2 while ( true) {
3
// cherche le nœud à traiter
4
u =
nœud avec le plus petit coût et non traité dans la
5
table T
6
ou
7
MARQUEUR_ARRÊT si ce nœud n’existe pas
8
if (u == MARQUEUR_ARRÊT)
9
return;
10
T[u].traité = true;
11
// pour chaque nœud adjacent de u faire …
12
while ((v = adjacent(u, lna)) != vide) {
13
if (!T[v].traité)
14
// Est-ce un coût minimal ?
15
// c(u, v) est la valeur de l’arête reliant les
16
// les nœuds u et v
17
if (T[u].coût + c(u, v) < T[v].coût) {
18
// ajuster le coût et indique le nœud prédécesseur
19
T[v].coût = T[u].coût + c(u, v);
20
T[v].chemin = u;
21
}
22
}
23 }
}
Le réglage initial de la table des données ne doit pas poser de problème. Le seul point
important est d’assigner au nœud de départ un coût zéro et pour tous les autres
nœuds un coût infini (ou très grand). La fonction ParcoureTable() est simplement
une fonction récursive qui imprime un chemin à coût minimal à partir des nœuds
prédécesseurs contenus dans la table. Cette fonction est utilisée une fois tous les
chemins minimaux auront été identifiés par l’algorithme de Dijsktra.
L’algorithme de Dijsktra consiste en une boucle infinie (ligne 2). On doit toujours
traiter le nœud dont le coût (estimé) est le plus petit et qui n’est pas encore traité (ligne
4 à 7). Le nœud sélectionné est marqué comme traité (ligne 10). S’il n’y a pas de
nœuds de cette nature alors l’algorithme doit terminer son exécution puisque tous les
chemins à coûts minimaux ont été identifiés.
L’algorithme utilise le parcours systématique en « largeur d’abord » d’un graphe pour
identifier tous les chemins dont le coût est minimal. Le parcours en « largeur
d’abord » consiste à traiter tous les nœuds adjacents d’un nœud avant de traiter les
nœuds adjacents des nœuds adjacents (et ainsi de suite). Les chemins à coûts
minimaux sont donc construits en ajoutant les arêtes bouts à bouts à partir du nœud
de départ (source). Durant ce parcours systématique du graphe (ligne 12), l’algorithme
prend note du coût du chemin de chacun des nœuds rencontrés. Il est également
9
« gourmand » puisque à chaque rencontre d’un nœud, l’algorithme sélectionne
toujours l’arête qui donne le plus petit coût au chemin courant (ligne 4). Le reste des
étapes de l’algorithme consiste à réajuster, au besoin, le coût des chemins et les nœuds
prédécesseurs (lignes 17 à 20).
Nous allons présenter un exemple montrant le principe d’opération de l’algorithme de
Dijkstra. Cet exemple utilise le graphe de la figure 4 et la liste de nœuds adjacents de la
figure 5. Rappelons que ce graphe représente un petit quadrilatère centre-ville.
4
1 5 7
5
6
4 6
5 3 8
7
4 8
8
7
1 Ste-Catherine - Mansfield
2 Ste-Catherine - McGill
3 Ste-Catherine - Université
4 Cathcart - Mansfield
2
700
4
300
5
600
3
1700
2
400
6
700
3
1
700
2 4
1 3
700
1
2
7
1200
8
5 Cathcart - McGill
6 Cathcart - Université
7 René Lévesque - Mansfield
8 René Lévesque - Université
• Figure 7 Quadrilatère des rues utilisés pour cet exemple.
L’objectif ici est d’obtenir les chemins minimaux entre le nœud 1 qui joue le rôle du
nœud de départ et les nœuds 5 et 8. D’abord initialisons la table des données.
1) Réglage initial de la table :
Nœud
Traité
Coût
Chemin
MARQUEUR_ARRÊT
1
false
0
MARQUEUR_ARRÊT
2
false
∞
MARQUEUR_ARRÊT
3
false
∞
MARQUEUR_ARRÊT
4
false
∞
MARQUEUR_ARRÊT
5
false
∞
MARQUEUR_ARRÊT
6
false
∞
MARQUEUR_ARRÊT
7
false
∞
MARQUEUR_ARRÊT
8
false
∞
Note : À l’état initial seulement le nœud de départ a un coût initial de zéro.
10
2) Exécution de l’algorithme de Dijkstra :
►
i)
Sélectionner le nœud 1, u = 1 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[1].coût est 0.
T[1].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 2.
a) T[1].coût + c(1, 2) < T[2].coût ?
0 + 400 < ∞ → vrai alors T[2].coût = 400, T[2].chemin = 1.
prendre un nœud v qui est adjacent à u. v = 4
b) T[1].coût + c(1, 4) < T[4].coût ?
0 + 700 < ∞ → vrai alors T[4].coût = 700, T[4].chemin = 1.
iii)
L’état actuel de la table des données
Nœud
1
2
3
4
5
6
7
8
Traité
true
false
false
false
false
false
false
false
Coût
0
400
∞
700
∞
∞
∞
∞
Chemin
MARQUEUR_ARRÊT
1
MARQUEUR_ARRÊT
1
MARQUEUR_ARRÊT
MARQUEUR_ARRÊT
MARQUEUR_ARRÊT
MARQUEUR_ARRÊT
►
i)
Sélectionner le nœud 2, u = 2 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[2].coût est 400.
T[2].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 1.
a) nœud 1 est déjà traité passe au nœud adjacent suivant.
prendre un nœud v qui est adjacent à u. v = 3.
11
b) T[2].coût + c(2, 3) < T[3].coût ?
400 + 700 < ∞ → vrai alors T[3].coût = 1100, T[3].chemin = 2.
iii)
L’état actuel de la table des données
Nœud
1
2
3
4
5
6
7
8
Traité
true
true
false
false
false
false
false
false
Coût
0
400
1100
700
∞
∞
∞
∞
Chemin
MARQUEUR_ARRÊT
1
2
1
MARQUEUR_ARRÊT
MARQUEUR_ARRÊT
MARQUEUR_ARRÊT
MARQUEUR_ARRÊT
►
i)
Sélectionner le nœud 4, u = 4 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[4].coût est 700.
T[4].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 1.
a) nœud 1 est déjà traité.
prendre un nœud v qui est adjacent à u. v = 5.
b) T[4].coût + c(4, 5) < T[5].coût ?
700 + 300 < ∞ → vrai alors T[5].coût = 1000, T[5].chemin = 4.
prendre un nœud v qui est adjacent à u. v = 7.
b) T[4].coût + c(4, 7) < T[7].coût ?
700 + 700 < ∞ → vrai alors T[7].coût = 1400, T[7].chemin = 4.
iii)
L’état actuel de la table des données
Nœud
1
2
3
4
5
Traité
true
true
false
true
false
Coût
0
400
1100
700
1000
12
Chemin
MARQUEUR_ARRÊT
1
2
1
4
6
7
8
false
false
false
∞
1400
∞
MARQUEUR_ARRÊT
4
MARQUEUR_ARRÊT
►
i)
Sélectionner le nœud 5, u = 5 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[5].coût est 1000.
T[5].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 4.
a) nœud 4 est déjà traité.
prendre un nœud v qui est adjacent à u. v = 6.
b) T[5].coût + c(5, 6) < T[6].coût ?
5.
iii)
1000 + 600 < ∞ → vrai alors T[6].coût = 1600, T[6].chemin =
L’état actuel de la table des données
Nœud
1
2
3
4
5
6
7
8
Traité
true
true
false
true
true
false
false
false
Coût
0
400
1100
700
1000
1600
1400
∞
Chemin
MARQUEUR_ARRÊT
1
2
1
4
5
4
MARQUEUR_ARRÊT
►
i)
Sélectionner le nœud 3, u = 3 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[3].coût est 1100.
T[3].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 2.
a) nœud 2 est déjà traité.
iii)
L’état actuel de la table des données
13
Nœud
1
2
3
4
5
6
7
8
Traité
true
true
true
true
true
false
false
false
Coût
0
400
1100
700
1000
1600
1400
∞
Chemin
MARQUEUR_ARRÊT
1
2
1
4
5
4
MARQUEUR_ARRÊT
►
i)
Sélectionner le nœud 7, u = 7 puisque son coût est le plus petit parmi les
nœuds pas encore traité. T[7].coût est 1400.
T[7].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 4.
a) nœud 4 est déjà traité.
prendre un nœud v qui est adjacent à u. v = 8.
b) T[7].coût + c(7, 8) < T[8].coût ?
7.
iii)
1400 + 1200 < ∞ → vrai alors T[8].coût = 2600, T[8].chemin =
L’état actuel de la table des données
Nœud
1
2
3
4
5
6
7
8
Traité
true
true
true
true
true
false
true
false
Coût
0
400
1100
700
1000
1600
1400
2600
Chemin
MARQUEUR_ARRÊT
1
2
1
4
5
4
7
►
i)
Sélectionner le nœud 6, u = 6 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[6].coût est 1600.
T[6].traité = true
14
ii)
prendre un nœud v qui est adjacent à u. v = 3.
b) nœud 3 est déjà traité.
prendre un nœud v qui est adjacent à u. v = 8.
b) T[6].coût + c(6, 8) < T[8].coût ?
1600 + 700 < 2600 → vrai alors T[8].coût = 2300, T[8].chemin
= 6.
iii)
L’état actuel de la table des données
Nœud
1
2
3
4
5
6
7
8
Traité
true
true
true
true
true
true
true
false
Coût
0
400
1100
700
1000
1600
1400
2300
Chemin
MARQUEUR_ARRÊT
1
2
1
4
5
4
6
►
i)
Sélectionner le nœud 8, u = 8 puisque son coût est le plus petit parmi les
nœuds pas encore traités. T[8].coût est 2300.
T[8].traité = true
ii)
prendre un nœud v qui est adjacent à u. v = 7
a) nœud 7 est déjà traité.
iii)
L’état actuel de la table des données
Nœud
1
2
3
4
5
6
7
8
Traité
true
true
true
true
true
true
true
true
Coût
0
400
1100
700
1000
1600
1400
2300
15
Chemin
MARQUEUR_ARRÊT
1
2
1
4
5
4
6
►
i)
Il n’y a plus de nœud non traité. L’algorithme doit terminer son
exécution.
Note : La dernière table des données donne le résultat de l’algorithme de
Dijsktra.
3) Affichage des chemins minimaux : Nœud de départ 1, nœuds d’arrivée 5 et 8.
ParcoureTable(5, T[]);
►
T[5].chemin → nœud 4 → coût 1000
T[4].chemin → nœud 1
T[1].chemin → MARQUEUR_ARRÊT
Affichage :
1 à 4 à 5 : coût du chemin 1000
ParcoureTable(8, T[]);
►
T[8].chemin → nœud 6 → coût 2300
T[6].chemin → nœud 5
T[5].chemin → nœud 4
T[4].chemin → nœud 1
T[1].chemin → MARQUEUR_ARRÊT
Affichage :
1 à 4 à 5 à 6 à 8 : coût du chemin 2300
16
Téléchargement