Consignes
La collaboration orale est encouragée, mais le partage de code et le copier/coller (via Discord, github, etc.) n'est pas autorisé et pourra être pénalisé.
Le rendu de ce TP se fera uniquement par TPLab et consistera en une archive (zip, tar, etc.) contenant un répertoire TP2_NOM avec au minimum :
-
votre fichier shortest_NOM.py,
-
votre fichier seam_carving_NOM.py,
-
un fichier README.txt contenant les réponses aux questions et vos remarques et observations.
Vous pouvez ajouter d'autres fichiers s'ils sont documentés dans votre fichier README.txt.
Vous aurez compris que NOM devra être remplacé par votre nom (ou vos noms en cas de binômes) !
Tous vos fichiers doivent comporter une entête avec votre nom et votre groupe de TP.
Tout non respect d'une ou plusieurs de ces consignes entrainera automatiquement un retrait de points sur votre note !
Liens utiles
-
Lien vers l'archive du TP TP2_ALONZO.tar.gz.
Cette archive contient
-
des classe pour les graphes pondérés en listes d'adjacences : graph.py,
-
le code à compléter pour les parties 1 et 3 : shortest_ALONZO.py,
-
le code à compléter pour la partie 2 : seam_carving_ALONZO.py, avec le fichier naive_ppm.py pour charger les images au format ppm,
-
quelques exemples de graphes dans le répertoire graph_examples,
-
des exemples d'images libres pour la partie 2 dans le répertoire Images (au format ppm)
-
quelques exemples de grilles pour la dernière partie dans le répertoire grid_examples,
-
-
2 gros graphes pour la question 3 : gros_graphe1.txt, gros_graphe2.txt
-
lien vers l'article original de Shai Avidan et Ariel Shamir : "Seam Carving for Content-Aware Image Resizing"
1. Algorithme de Dijsktra
1.1. Rappels : graphe pondéré
Le parcours en largeur permet de trouver le plus court chemin dans un graphe, lorsqu'on compte le nombre d'arêtes. Il ne fonctionne pas lorsque les arêtes ne sont pas toutes équivalentes, comme le montre l'exemple suivant :
Un parcours en largeur trouvera un chemin de longueur 1 pour aller de Chambéry à Saint Etienne, mais la durée correspondante est loin d'être optimale.
On parle de graphe pondéré lorsque les arêtes ont un poids associé.
Le fichier graph.py est une variante de celui du TP qui gère
les graphes pondérés. Les listes d'adjacence contiennent des paires
(poids, voisin)
au lieu de voisins simples. On peut donc
parcourir les voisins du sommet s
avec
for p, v in G[s]:
...
-
Écrivez dans le fichier shortest_ALONZO.py une fonction
max_out
qui prend un grapheG
en argument et renvoie le sommet dont la somme des poids sortants est maximale.Pour tester cette fonction, la complétez la fonction
main_test
pour qu'elle appelle la fonctionmax_out
et affiche le résultat.Vous pourrez testez en donnant l'option -T sur la ligne de commande
$ python3 ./shortest_ALONZO.py -T graph_examples/graph1.txt fonction main_test le graphe contient 3 sommets le sommet avec flot sortant max est : 1
-
Écrivez également une fonction
path_weight
qui prend en argument un grapheG
et une liste de sommetsP
. Votre fonction devra renvoyer la somme des poids des arêtes du chemin défini parP
.Si les sommets de
P
ne forment pas un chemin, la fonction devra renvoyerNone
.Vous pouvez tester la fonction en décommentant la fin de la fonction
main_test
:$ python3 ./shortest_ALONZO.py -T graph_examples/graph1.txt 0 1 2 0 fonction main_test le graphe contient 3 sommets flot sortant max d'un sommet du graphe : 2055.0 le chemin a un poids total de 2105.0
N'hésitez pas à utiliser la fonction main_test
pour faire
d'autres tests au cours du TP.
1.2. Plus court chemin : algorithme de Dijkstra
Échauffement : parcours en largeur
-
Dans le fichier shortest-ALONZO.py écrivez une fonction
BFS(G, v0, v1)
qui fait un parcours en largeur du grapheG
et qui s'arrête lorsqu'elle rencontrev1
.Cette fonction ressemble beaucoup à la fonction BFS (ou BFS_opt si vous préférez cette version) du TP 1 car le poids des arêtes n'est pas utilisé pour le choix des sommets visités.
En plus du tableau
Pred
(comme dans le TP 1), la fonctionBFS
doit également renvoyer un tableauD
contenant les poids des chemins dev0
aux sommets parcourus. Pour calculer ce tableauD
, il suffit-
d'initialiser
D[v0]
à0
etD[s] = ∞
pour tous les autres sommets. (On suppose que tous les poids sont des flottants. La constanteoo
définie dans le fichier shortest-ALONZO.py est justement le flottant+∞
.) -
de modifier
D[v]
lorsqu'on définit son prédécesseur : siPred[v] = s
, alorsD[v] = D[s] + d
lorsqued
est le poids de l'arête des
versv
.
Pour tester, vous pouvez lancer la commande suivante :
$ python3 ./shortest_ALONZO.py -A BFS graph_examples/graph2.txt found path of weight 4.5580891922215 from 0 to 19
Par défaut, le parcours est appelé avec
v0=0
etv1=N-1
. Vous pouvez changer ceci avec les options--start
et--end
:$ python3 ./shortest_ALONZO.py -A BFS --start=7 --end=12 graph_examples/graph2.txt found path of weight 11.649078967534761 from 7 to 12
Suivant le parcours en largeur que vous avez implémenté, les valeurs peuvent changer.
-
-
Le poids calculé dans le tableau
D
correspond il à la plus petite distance depuisv0
?
Algorithme de Dijkstra
Comme le parcours en largeur, l'algorithme de Dijkstra peut se reconstruire à partir du parcours en profondeur basique, avec les modifications suivantes.
-
Pour chaque sommet visité, on conserve sa distance actuelle depuis
v0
dans un tableauD
. Initialement,D[v0] = 0
, etD[v] = ∞
pour tous les autres sommets. -
Le choix du sommet dans
ATraiter
est fait en prenant le sommets
avec une valeur deD[s]
minimale. -
Lorsqu'on regarde un voisin
v
des
, on connait-
la valeur de
D[s]
-
le poids
p
de l'arête qui va des
àv
.
Cela donne un nouveau chemin pour aller de
v0
àv
: aller dev0
às
, puis des
àv
. Le poids de ce chemin est égal àD[s] + d
.Si cette valeur est inférieure à la valeur actuelle de
D[v]
on metPred[v]
etD[v]
à jour et on ajoutev
dansATraiter
.Sinon, on ne fait rien : on a déjà un chemin de
v0
àv
plus court. -
-
Écrivez la fonction
extract_best(ATraiter, D)
qui recherche le sommet deATraiter
avec une valeur dansD
minimale. La fonction devra renvoyer le sommet trouvé et le supprimer dans le tableauATraiter
. -
Écrivez la fonction
shortest_path0(G, v0, v1)
qui implémente l'algorithme décrit ci dessus. Vous pouvez partir de la fonctionDFS
du TP 1 et la modifier en ajoutant le tableauD
et la gestion des voisins. (Vous devrez également remplacer l'appels = ATraiter.pop()
par un appel à la fonctionextract_best
.)Attention, on ne met
Pred
etD
à jour que lorsque la nouvelle valeur deD[v]
est meilleure (strictement plus petite) que l'ancienne.Vous pouvez appeler cet algorithme de parcours avec -A shortest_path0 :
$ python3 ./shortest_ALONZO.py -A shortest_path0 --start=9 --end=5 graph_examples/graph2.txt found path of weight 6.0890260575204085 from 9 to 5
-
Lorsque tous les poids sont égaux à
1.0
, l'algorithme de Dijkstra et le parcours en largeur doivent donner des chemins de même poids. Vérifiez sur des exemples.Vous pouvez générer des graphes aléatoires avec la commande
$ python3 ./shortest_ALONZO.py --random-graph N D 1.0
qui génèrera un graphe à N sommets, où pour chaque sommet, D voisins ont été tirés aléatoirement. Vous pouvez sauvez le résultat dans un fichier et le réutiliser dans la suite :
$ python3 ./shortest_ALONZO.py --random-graph 200 2 1.0 > test-graph.txt $ python3 ./shortest_ALONZO.py -A BFS,shortest_path0 test-graph.txt ...
-
Comparez les vitesses d'exécution des 2 algorithmes sur de gros graphes aléatoires.
-
En utilisant le profiler intégré de Python, chercher d'où vient la différence de temps de calcul entre
BFS
etshortest_path0
.Rappel : vous pouvez lancer un script Python en mode "profiling" avec
$ python3 -m cProfile -s tottime shortest_ALONZO.py ... ...
Attention, l'exécution du script est plus lente lorsqu'il est exécuté en mode "profiling".
Si vous n'avez pas le module cProfile, vous pouvez essayer avec -m profile.
1.3. De l'intérêt de l'algorithmique : file de priorité
Les files de priorités sont une structure de données avec les opérations suivantes :
-
insertion d'un élément,
-
recherche et suppression de l'élément minimal (ou maximal, suivant les implémentations).
Le module heapq
de la librairie standard de Python offre une
implémentation de cette structure de données en Python.
Nous voulons utiliser cette structure pour insérer des sommets avec une
distance, et rechercher / supprimer l'élément de distance minimale. Il suffit
d'utiliser une file de priorité avec des paires (distance,
sommet)
. (Ça marche car l'ordre sur les paires est l'ordre
lexicographique.)
L'exemple suivant montre comment créer une file de priorité, y insérer les
sommet 0
, 2
et 1
avec des distances
arbitraires, et récupérer l'élément de distance minimale :
$ python3
>>> from heapq import heapify, heappush, heappop
>>> ATraiter = []
>>> heapify(ATraiter)
>>> heappush(ATraiter, (78.5, 0))
>>> heappush(ATraiter, (3.7, 2))
>>> heappush(ATraiter, (123.0, 1))
>>> heappop(ATraiter)
(3.7, 2)
>>> heappop(ATraiter)
(78.7, 2)
>>> heappop(ATraiter)
(123.0, 1)
Attention, il ne faut pas oublier d'insérer les sommets avec leur
priorité : heappush(ATraiter, (PRIORITÉ, SOMMET))
.
-
Écrivez la fonction
shortest_path_dijkstra(G, v0, v1)
en reprenant le code deshortest_path0
, mais en utilisant une file de priorité. -
Vérifiez que les 2 fonctions donnent bien le même résultat sur des graphes aléatoires.
-
Comparez l'efficacité des fonctions
BFS
,shortest_path0
etshortest_path_dijkstra
sur des gros graphes, et en utilisant le profiler de Python.
2. Interlude : application au "recadrage intelligent" d'image
2.1. Description du problème
Une application surprenante de l'algorithme de Dijkstra est le "recadrage intelligent" d'images" ("seam carving" en anglais).
Le problème est le suivant : comment rétrécir une image (changer la largeur sans changer la hauteur), sans la déformer ?
Les méthodes naïves transforment l'image de gauche, en l'image de droite :
Les bulles ne sont plus rondes, et le garçon est déformé par la transformation.
L'objectif est d'obtenir l'image suivante, plus fidèle à la "réalité". (Passez la souris sur l'image pour visualiser la différence.)
L'idée est assez simple : pour passer de l'image originale (375x250) à l'image finale (250x250), 125 "chemins" ont été supprimés. L'image passe donc par des étapes intérmédiaires de tailles (374x250), (373x250), ...
Voici 2 exemples de chemins supprimés, visualisés en rouge :
étape 1 | étape 116 |
Chaque chemin est une suite de pixel de l'image, un par ligne, qui se touchent. À chaque étape, nous supprimons le chemin de "poids" minimal, que l'on trouve ... en utilisant l'algorithme de Dijkstra.
Visuellement, la transformation ressemble à
On voit bien que les chemins choisis évitent les parties "saillantes" de l'images.
-
Ce type de manipulation d'image est assez gourmand en ressources et Python n'est pas le langage le plus approprié.
Le recadrage de l'image ci dessus a pris environ 25 secondes sur mon vieux portable, et 3 minutes 40 secondes pour la version 750x500 pixels ! Un langage compilé serait de loin préférable.
-
Attention, les images présentes sur la page du sujet sont au format .png par soucis de compatibilité avec les navigateurs. Pour tester votre code, vous devrez utilisez des images au format .ppm comme celles présentes dans le répertoire Images de l'archive du TP.
-
Si vous voulez tester sur d'autres images que celles fournies, il faudra les convertir au format .ppm. Si vous êtes sous Linux, l'utilitaire convert (du paquet imagemagick sous Debian / Ubuntu / ...) permet de le faire facilement :
$ convert MON_IMAGE -resize 300x300 RESULTAT.ppm
convertira l'image du fichier MON_IMAGE (au format, .jpg, .png, etc.) au format .ppm et la redimensionnera pour qu'elle tienne dans un carré de 300 pixels de large. Le résultat sera sauvegardé dans le fichier RESULTAT.ppm.
-
Pour visualiser les images .ppm, vous pouvez utiliser la commande display. (Elle fait partie du paquet imagemagick.)
$ display IMAGE1 IMAGE2 ...
Voici quelques exemples supplémentaires :
image originale | après recadrage |
---|---|
2.2. Détails
Le fichier fourni construit un graphe à partir d'une image en reliant chaque pixel aux 3 pixels qui le touchent sur la ligne suivante.
De manière surprenante, dans cette version, le poids d'une arête (x,y) → (x', y'), ne dépend que du pixel d'arrivée (x',y').
Les pixels avec des voisins similaires sont associés à un petit poids : ce sont ceux qui se "fondent" dans leur voisinage et que l'on peut supprimer sans provoquer de discontinuité dans l'image.
L'objectif est de trouver le chemin de poids minimal entre un pixel de la première ligne (y=0) et un pixel de la dernière ligne (y=HAUTEUR-1).
Vous n'avez pas à calculer ces poids, mais pour les curieux, le poids associé aux pixel (x,y) est égal à
On calcule la somme de la distance entre les pixels au dessus et au dessous (premier terme de la somme) et celle entre les pixels à droite et à gauche (second terme de la somme).
Les pixels avec un poids élevé (en blanc ci dessous) sont les pixels de "transitions" entre parties similaires de l'image :
L'algorithme de Dijkstra évitera le plus possible ces pixels blancs car ils correspondent à des arêtes de poids élevé.
Complétez la fonction find_seam(img)
qui utilise l'algorithme de
Dijkstra pour chercher le chemin de poids minimal dans l'image
img
. La fonction doit renvoyer une paire S, e
où
-
S
est le chemin trouvé, donnés par les abscisses[x0, x1, x2, ..., x{HAUTEUR-1}]
des pixels du chemin, -
e
est l'énergie du chemin, càd la longueur trouvé par l'algorithme de Dijkstra dans le tableauD
.
Le chemin S
peut être extrait de l'arbre Pred
calculé par l'algorithme de Dijkstra.
-
L'argument
img
defind_seam
du fichier seam_carving_ALONZO.py est une image. Vous aurez besoin-
de sa taille, accessible avec
img.width
etimg.height
-
de l'énergie associée à un pixel
x,y
, accessible avecimg.E[y][x]
. C'est avec ça qu'on calcule le poids d'une arête? ⟶ (x,y)
.
La valeur
oo
définit au début de la fonction est l'énergie maximale possible d'un chemin. Elle peut servir de valeur "infinie" pour les pixels non encore atteints. -
-
Pour simplifier le code, manipulez les sommets du graphe implicite comme des paires de coordonnées
(x,y)
plutôt que comme des entiers. Cela vous évitera de faire des conversions entre les 2 représentations. Pour cela, vous devrez donc :-
utiliser un dictionnaire
Pred
pour associer les coordonnées du pixel précédent à un nouveau pixel, -
utiliser un ensemble
Vu
contenant les coordonnées de pixels déjà rencontrés,Vous pouvez initialiser
Vu
avecVu = set()
et y ajouter des pixels avecVu.add((x, y))
. -
utiliser un dictionnaire
D
pour enregistrer la plus petite distance trouvée pour chaque pixel. Lorsqu'un pixel n'est pas dans le dictionnaire, la distance associé est simplementoo
. (Vous pouvez utiliser la méthode.get
des dictionnaires pour faire cette opération facilement.)
La file de priorité
ATraiter
reste une file de priorité. Elle contiendra des triplets(energie, x, y)
. -
-
Attention, comme il y a plusieurs sommets de départ possible (tous ceux de la première ligne), l'initialisation de
Vu
,D
etATraiter
sera légèrement plus complexe que dans la version de l'algorithme de Dijkstra de la partie précédente.D'autre part, l'algorithme devra s'arrêter dès qu'un pixel sur la dernière ligne est rencontré.
-
Pour tester, vous pouvez lancer la commande
$ python3 ./seam_carving_ALONZO.py IMAGE [NB]
IMAGE doit être un fichier image au format .ppm, pas trop gros. Vous trouverez quelques exemples dans l'archive du TP (répertoire Images).
Vous pouvez précisez le nombre de chemins à supprimer avec l'argument NB. Vous obtiendrez alors un fichier IMAGE-carved.ppm contenant le résultat.
Si vous ne donnez pas l'argument NB, un seul chemin sera supprimé et vous obtiendrez un fichier supplémentaire IMAGE-000-seam.ppm contenant l'image originale avec le chemin trouvé en rouge.
Voici quelques exemples de chemins trouvés à la première étape :
fichier family-small.ppm fichier gulls-small.ppm fichier soap-bubbles-small.ppm
Sur certaines images, il y a de nombreux chemins de poids minimal. Si on supprime le premier (plus à gauche), le résultat est "tassé sur la gauche" :
Décrivez et implémentez une manière d'obtenir un résultat plus "équilibré" sur ce type d'images :
Il y a de nombreuses manières d'améliorer et de généraliser cette méthode.
-
On peut utiliser une meilleure fonction de poids pour les chemins ("forward energy").
-
On peut utiliser un masque sur l'image pour donner des poids très élevés à certaines zones. Cela permet de garantir qu'on ne les modifiera pas.
-
On peut aussi utiliser un masque sur l'image pour donner des poids très faibles à certaines zones. Cela permet de supprimer certaines parties de l'image ! (Vous aviez vu que le garçon sur la plage avait disparu ?)
-
On peut améliorer la recherche des chemins. Lorsqu'on supprime un chemin, une grande partie des calculs faits peut être réutilisée. (On verra une technique similaire dans le TP3.)
-
On peut utiliser les chemins de poids minimal pour augmenter la largeur d'un pixel : on insère un chemin à gauche (ou à droite) du chemin minimal en prenant la moyenne des pixels de droite et de gauche. (Il y a une petite subtilité si on veut faire ça plusieurs fois...)
3. Plus court chemin avec heuristiques : A*
L'algorithme de Dijkstra choisit à chaque étape le sommet avec
D[s]
minimal. Lorsque tous les poids sont identiques, cela
revient à faire un parcours en largeur car on commence par v0
(D[v0]=0
), puis on passe aux voisins de v0
(D[s]=1
), puis les voisins des voisins de v0
(D[s]=2
), etc.
On peut ainsi calculer la distance de v0
à tous les autres
sommets avec un seul parcours.
Un désavantage de cette méthode est que lorsqu'on est uniquement intéressé
par la distance entre v0
et un sommet particulier
v1
, l'algorithme de Dijkstra ou le parcours en largeur explorent
de nombreux sommets "inutiles" avant d'atteindre v1
.
Dans certains cas, on peut estimer la "direction" de v1
et se
concentrer sur les sommets qui nous "rapprochent" de v1
.
Nous allons regarder 2 algorithmes de ce style :
-
"best first search", qui essaie de trouver un chemin rapidement, peu importe s'il est optimal,
-
"A*", qui essaie de trouver un chemin optimal plus rapidement que l'algorithme de Dijkstra.
3.1. "Best-first search" sur une grille
Nous allons reconsidérer des graphes provenant d'une grille carrée, du style
..###...##..#.#..#....#..##
..#.#.##..#......#..#...#.#
.#...#..#.....#....#.......
#.#.#.##....#....##.##.....
.........#.......#.#..#..##
.#........##...#..##..#....
#..##.#...#.#..##...#.#.#..
......#.....#.....#...#.#..
#......#..#.#........#..##.
##..#...#...#.....#.#......
..........................#
..........###.....#....#.##
#.........#..##......#.....
......#.......#..#....#....
...##...#.........##.#.....
Les cases .
sont libres, et les cases #
sont
occupées : chaque case a donc entre 0 et 4 voisines disponibles. (Les
déplacements en diagonal ne sont pas autorisés.)
Les labyrinthes du TP1 peuvent être vu comme de telles grilles :
#####################
#.#...#.............#
#.#.#.#.#########.#.#
#...#.......#...#.#.#
#.#########.###.#.#.#
#...#...#.#.....#.#.#
#.###.#.#.#.#####.#.#
#.#...#.#...#.....#.#
#.#.###.#####.#####.#
#.#...#.......#.....#
#####################
À la différence des premiers labyrinthes du TP 1 où seuls les caractères à coordonnées impaires étaient des sommets de graphe, tous les caractères sont bien des sommets.
Ces graphes ressemblent donc plus aux graphes de la seconde partie du TP 1.
Le parcours "best first search" est basé sur le parcours en profondeur basique. Le sommet
s
extrait de ATraiter
est celui qui nous rapproche
le plus du but à atteindre.
Comme la distance au but est inconnue (il faudrait faire un parcours de graphe pour la connaitre exactement), on l'approxime par une distance connue et facile à calculer : la distance au but si on suppose qu'il n'y a pas d'obstacle.
-
Si
s
etv
sont 2 sommets d'une grilleG
, comment peut-on calculer (en O(1)) la distance sans obstacle entres
etv
. (Note : vous pouvez récupérer les coordonnées d'un sommetv
avecG.xy(v)
.) -
Programmez la fonction
best_first_search(G, v0, v1)
qui cherche un chemin dev0
àv1
en commençant par les sommets qui "se rapprochent" dev1
.Comme pour le parcours en largeur de la première partie, calculez également le tableau
D
donnant la distance (qui tient compte des poids des arêtes) des chemins explorés avecPred
. -
Comparez l'efficacité (en temps) du parcours en largeur usuel (partie 1) et ce nouveau parcours sur des grilles de tailles différentes.
-
Vous pouvez générer des grilles aléatoires avec l'option --random-grid. Vous devrez préciser la largeur et hauteur, et ls proportions relatives des cases. Par exemples :
$ python3 ./shortest_ALONZO.py --random-grid 20 10 "....#" ...
va générer une grille de largeur 20 et hauteur 10, où il y a environ 4 fois plus de
.
que de#
. -
Vous pouvez rediriger la grille dans un fichier avec une redirection, et l'utiliser comme un fichier de graphe standard :
$ python3 ./shortest_ALONZO.py --random-grid 20 10 "....#" > ma_grille.txt $ python3 ./shortest_ALONZO.py -A best_first_search --start=15,7 --end=19,9 ma_grille.txt found path of weight 12 from 155 to 199 ###################### # # # # # # # # # # # # # .. # # #@*#..# # .****# # ..#$# ######################
Notes :
-
les caractères
*
correspondent aux cases du chemin trouvé par l'algorithme, -
les arêtes ont un poids de
2
, ce qui explique le poids total affiché (12), -
les cases vides (espace) correspondent aux cases non explorées par l'algorithme.
-
-
-
Pouvez vous construire des grilles où le parcours "best first search" a une meilleure complexité (en temps) que le parcours en largeur ?
3.2. Des grilles plus complexes
On place maintenant des tapis roulants sur certaines cases :
-
un tapis
>
déplace son contenu vers la droite, -
un tapis
<
déplace son contenu vers la gauche, -
un tapis
^
déplace son contenu vers le haut, -
un tapis
v
déplace son contenu vers le bas.
Les cases .
sont toujours des cases vides, et les #
des cases inaccessibles.
Les cases >
, <
, v
et
^
aident les déplacements dans leur direction et entravent les
déplacements dans la direction opposée. L'énergie nécessaire pour un
changement de case est calculé comme suit :
case de départ | vers la droite | vers la gauche | vers le haut | vers le bas |
---|---|---|---|---|
>
|
1 | 4 | 3 | 3 |
<
|
4 | 1 | 3 | 3 |
v
|
3 | 3 | 4 | 1 |
^
|
3 | 3 | 1 | 4 |
.
|
2 | 2 | 2 | 2 |
L'objectif est de trouver un chemin de cout minimal pour aller d'une case à une autre. Par exemple, pour la grille suivante (fichier grid_examples/grid-1.txt)
v.>vv
^<^>>
v>^v>
^v.>.
.vv.<
il existe une solution de poids 14 pour aller du coin au haut à gauche au coins en bas à droite :
$ python3 ./shortest_ALONZO.py -A shortest_path_dijkstra --start 0,0 --end 4,4 ./grid_examples/grid-1.txt
found path of weight 14 from 0 to 24
#######
#@***v#
#^<^*>#
#v>^*>#
#^v.**#
#.vv.$#
#######
Pour aller du coin en bas à droite au coin en haut à gauche, le meilleurs chemin à un poids de 15 :
$ python3 ./shortest_ALONZO.py -A shortest_path_dijkstra --start 4,4 --end 0,0 ./grid_examples/grid-1.txt
found path of weight 15 from 24 to 0
#######
#$.>vv#
#***>>#
#v>**>#
#^v.*.#
#.vv*@#
#######
Lors de l'affichage des solutions :
-
le caractère
@
est le point de départ, -
le caractère
$
est le point de départ, -
un caractère
*
fait partie du chemin trouvé, il "cache" le vrai contenu de la case (.<>v^
), -
un caractère
.<>v^
a été exploré lors du parcours, mais ne fait pas partie du chemin trouvé, -
un caractère
-
Vérifiez que votre fonction
shortest_path_disjkstra
donne le bon résultat. -
Quels sont les poids des chemins trouvés par
BFS
etbest_first_search
. Expliquez avec des mots d'où viennent les différences.
3.3. Algorithme A*
L'algorithme "best first search" choisit toujours le sommet le plus proche du
but pour la fonction de distance approchée. Pour copier l'algorithme de
Dijkstra et espérer trouver un chemin optimal, l'algorithme A* choisit le
sommet s
qui minimise D[s] + h(s)
:
-
D[s]
est la meilleure distance réelle (somme des poids des arêtes) de l'originev0
às
trouvée à ce moment, -
h(s)
est la distance approchée des
au butv1
, calculée de la même manière que dans la fonctionbest_first_search
.
-
Codez la fonction
a_star(G, v0, v1)
. -
Vérifiez qu'elle trouve un chemin optimal en comparant son résultat avec celui de
shortest_path_dijkstra
. -
En utilisant le profileur de Python, comparer l'efficacité de
shortest_path_dijkstra
eta_star
.
Vous pouvez générer des grilles aléatoires avec l'option --random-grid. Par exemple :
$ python3 ./shortest_ALONZO.py --random-grid 30 10 ".<>v^"
...
génère une grille 30x10 où les cases sont aléatoirement choisies parmi .<>v^. Pour obtenir une grille avec quelques obstacles, beaucoup des cases libre et des tapis roulants, on pourrait exécuter :
$ python3 ./shortest_ALONZO.py --random-grid 30 10 ".....<>v^#"
...
3.4. A*, approximation
On modifie maintenant le cout des déplacements. Si on est sur une case
>
, le cout d'aller vers la droite est 0
. Il
suffit effectivement d'attendre que le tapis nous dépose sur la case de
droite.
Les poids sont
case de départ | vers la droite | vers la gauche | vers le haut | vers le bas |
---|---|---|---|---|
>
|
0 | 3 | 2 | 2 |
<
|
3 | 0 | 2 | 2 |
v
|
2 | 2 | 3 | 0 |
^
|
2 | 2 | 0 | 3 |
.
|
1 | 1 | 1 | 1 |
Vous pouvez changer les poids du graphe associé à une grille avec l'option --weights="a,b,c,d" :
-
a correspond à un déplacement dans le sens du tapis,
-
b correspond à un déplacement depuis une case sans tapis,
-
c correspond à un déplacement perpendiculaire à un tapis,
-
d correspond à un déplacement à l'opposé du tapis.
La partie précédente utilisait implicitement l'option --weights="1,2,3,4", et il faut maintenant utiliser --weights="0,1,2,3".
-
Comparez l'efficacité (en temps) de
shortest_path_dijkstra
,best_first_search
eta_star
dans ce cas. Qu'en pensez vous ? -
Quelle est la propriété importante de la distance approchée qui n'est plus vérifiée et qui fait que A* ne donne plus la réponse optimale ?
-
Voyez vous des raisons d'utiliser A* (par rapport à
shortest_path_dijkstra
) dans ce cas ?