Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class City(Enum):
BORDEAUX = "Bordeaux"
DIJON = "Dijon"
LILLE = "Lille"
...
...s
```

Maintenant, vous pouvez utiliser des variables du type `City.BORDEAUX`, qui "représente" la string `"Bordeaux"` est qui est de type `City`.
Expand Down Expand Up @@ -140,13 +140,13 @@ Combien de calculs de distance avez-vous effectué pour parvenir au résultat Bo

Autrement dit, combien de fois avez vous mis à jour la distance pour un point du graphe ?

## Partie 2 : Amélioration avec l'algorithme A* (a-star)
## Partie 2 : Amélioration avec l'algorithme A\* (a-star)

Dijkstra garantit la meilleure solution possible. Mais vous avez sans doute remarqué qu'il s'aventure dans des recoins peu pertinents d'un point de vue "intelligent" (par exemple, tester Rouen dans un trajet Bordeaux -> Strasbourg...).

Or, il est parfois indispensable d'aller vite, quitte à trouver une solution "presque" optimale : c'est par exemple le cas du jeu vidéo.

Pour cela, l'algorithme A* (a star) propose d'associer une valeur heuristique à chaque sommet du graphe. C'est ce que vous pouvez voir ici en vert :
Pour cela, l'algorithme A\* (a star) propose d'associer une valeur heuristique à chaque sommet du graphe. C'est ce que vous pouvez voir ici en vert :

<img src="img/a-star.png">

Expand All @@ -158,7 +158,7 @@ La valeur est le temps qu'il faudrait à un cycliste pédalant à 16km/h pour pa

En utilisant ces heuristiques, résolvez "manuellement" (sur une feuille ou un document texte) le trajet Bordeaux -> Strasbourg.

**Important** : l'algorithme s'arrête dès qu'une solution est trouvée. Ce n'est pas nécessairement la meilleure, mais *souvent* l'une des meilleures.
**Important** : l'algorithme s'arrête dès qu'une solution est trouvée. Ce n'est pas nécessairement la meilleure, mais _souvent_ l'une des meilleures.

### 2.b Implémentation

Expand Down Expand Up @@ -238,7 +238,7 @@ Utilisez l'algorithme de Dijkstra pour résoudre le problème. Qu'observez-vous

### 3.c Algorithme SPFA

L'algorithme SPFA (*Shortest Path Faster Algorithm*, nom peu inspiré), est une variation de l'algorithme de Bellman-Ford, que nous ne verrons pas ici mais qui est très répandu.
L'algorithme SPFA (_Shortest Path Faster Algorithm_, nom peu inspiré), est une variation de l'algorithme de Bellman-Ford, que nous ne verrons pas ici mais qui est très répandu.

SPFA permet de trouver le meilleur chemin dans un graphe dont les arcs peuvent être de poids négatif, à condition qu'il n'y ait pas de **circuit absorbant**, c'est à dire de circuit "magique" qui permette de tendre vers moins l'infini. Vous pouvez le vérifier sur la carte, il n'y a pas d'astuce pour se faire de l'argent à l'infini.

Expand Down
25 changes: 25 additions & 0 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathfinder.heuristics import heuristics
from pathfinder.astar import AStar
from pathfinder.city import City
from pathfinder.graphs import graph, spfa_graph
from pathfinder.pathfinder import Pathfinder
from pathfinder.spfa import SPFA


def main():
dikjstra: Pathfinder = Pathfinder(graph)
dikjstra_result = dikjstra.get_shortest_path(
City.BORDEAUX, City.STRASBOURG
)
print(f"Dijkstra: {dikjstra_result['total']}")

astar: AStar = AStar(graph, heuristics)
astar_result = astar.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
print(f"Astar: {astar_result['total']}")

spfa: SPFA = SPFA(spfa_graph)
spfa_result = spfa.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
print(f"SPFA: {spfa_result['total']}")


main()
Binary file added pathfinder/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/astar.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/city.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/graphs.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/heuristics.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/pathfinder.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/spfa.cpython-312.pyc
Binary file not shown.
Binary file added pathfinder/__pycache__/types.cpython-312.pyc
Binary file not shown.
24 changes: 24 additions & 0 deletions pathfinder/astar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from pathfinder.city import City
from pathfinder.graphs import Graph
from pathfinder.pathfinder import Pathfinder


class AStar(Pathfinder):
heuristics: dict[City, float]

def __init__(self, graph: Graph, heuristics: dict[City, float]):
super().__init__(graph)
self.heuristics = heuristics

def get_next_city(self) -> City:
"""Return next city if present in unchecked cities,
else nearest city"""

if self.end in self.unchecked_cities:
return self.end
else:
return super().get_next_city()

def get_cost(self, city: City):
"""Override of get cost to take in heuristics"""
return super().get_cost(city) + self.heuristics[city]
16 changes: 16 additions & 0 deletions pathfinder/city.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from enum import Enum


class City(Enum):
BORDEAUX = "Bordeaux"
DIJON = "Dijon"
LILLE = "Lille"
LYON = "Lyon"
MARSEILLE = "Marseille"
NANTES = "Nantes"
PARIS = "Paris"
RENNES = "Rennes"
STRASBOURG = "Strasbourg"
TOULOUSE = "Toulouse"
ROUEN = "Rouen"
ORLEANS = "Orleans"
121 changes: 121 additions & 0 deletions pathfinder/graphs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
from pathfinder.city import City

Graph = dict[City, dict[City, float]]

graph: Graph = {
City.BORDEAUX: {
City.NANTES: 19,
City.ORLEANS: 24,
City.LYON: 31,
City.TOULOUSE: 14
},
City.DIJON: {
City.STRASBOURG: 20,
City.PARIS: 16,
City.ORLEANS: 15,
City.LYON: 11
},
City.LILLE: {
City.ROUEN: 12,
City.PARIS: 13,
City.STRASBOURG: 29
},
City.LYON: {
City.BORDEAUX: 31,
City.DIJON: 11,
City.MARSEILLE: 18,
City.ORLEANS: 20,
City.TOULOUSE: 28,
},
City.MARSEILLE: {
City.LYON: 18,
City.TOULOUSE: 22
},
City.TOULOUSE: {
City.BORDEAUX: 14,
City.LYON: 28,
City.MARSEILLE: 22
},
City.NANTES: {
City.BORDEAUX: 19,
City.RENNES: 6,
City.ROUEN: 17,
City.ORLEANS: 16
},
City.ORLEANS: {
City.BORDEAUX: 24,
City.DIJON: 15,
City.LYON: 20,
City.NANTES: 16,
City.PARIS: 7,
City.ROUEN: 11
},
City.PARIS: {
City.DIJON: 16,
City.LILLE: 13,
City.ORLEANS: 7,
City.ROUEN: 7,
City.STRASBOURG: 26
},
City.RENNES: {
City.NANTES: 6,
City.ROUEN: 17
},
City.ROUEN: {
City.LILLE: 12,
City.NANTES: 19,
City.ORLEANS: 11,
City.PARIS: 7,
City.RENNES: 17
},
City.STRASBOURG: {
City.DIJON: 20,
City.LILLE: 29,
City.PARIS: 26
}
}

spfa_graph: Graph = {
City.BORDEAUX: {
City.NANTES: 50,
City.TOULOUSE: 50
},
City.DIJON: {
City.STRASBOURG: 30,
},
City.LILLE: {
},
City.LYON: {
City.DIJON: 20
},
City.MARSEILLE: {
City.LYON: 30
},
City.TOULOUSE: {
City.LYON: -75,
City.MARSEILLE: 40
},
City.NANTES: {
City.ORLEANS: 10,
City.RENNES: 20
},
City.ORLEANS: {
City.STRASBOURG: 15,
City.PARIS: 40
},
City.PARIS: {
City.LILLE: 50,
City.STRASBOURG: -10,
City.ORLEANS: -30
},
City.RENNES: {
City.PARIS: 20,
City.ROUEN: 10
},
City.ROUEN: {
City.PARIS: -50
},
City.STRASBOURG: {
City.LILLE: 50
}
}
17 changes: 17 additions & 0 deletions pathfinder/heuristics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pathfinder.city import City


heuristics: dict[City, float] = {
City.BORDEAUX: 47,
City.DIJON: 15,
City.LILLE: 25,
City.STRASBOURG: 0,
City.ROUEN: 31,
City.PARIS: 25,
City.RENNES: 45,
City.ORLEANS: 27,
City.NANTES: 44,
City.LYON: 24,
City.TOULOUSE: 46,
City.MARSEILLE: 39
}
101 changes: 101 additions & 0 deletions pathfinder/pathfinder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from pathfinder.city import City
from pathfinder.graphs import Graph
from pathfinder.types import CitiesData, Path, DataToCity


class Pathfinder:
graph: Graph
unchecked_cities: CitiesData
checked_cities: CitiesData
end: City
start: City

def __init__(self, graph: Graph):
self.graph = graph

def get_shortest_path(self, start: City, end: City) -> Path | None:
self.start = start
self.end = end
self.checked_cities = {}
self.unchecked_cities = {
start: {
"previous_city": start,
"distance": 0
}
}
return self.calculate_shortest_path()

def calculate_shortest_path(self) -> Path | None:
current_city: City = self.start
while current_city != self.end:
self.execute_checks(current_city)
current_city = self.get_next_city()

self.checked_cities.update(
{current_city: self.unchecked_cities[current_city]}
)
return self.compute_path(current_city)

def execute_checks(self, city: City) -> None:
self.check_city(city)
self.checked_cities.update({city: self.unchecked_cities[city]})
del self.unchecked_cities[city]

def compute_path(self, last_city: City) -> Path | None:
if last_city is not None:
path: Path = {
"total": self.checked_cities[last_city]["distance"],
"steps": self.get_path_from_city(last_city)
}
return path
return None

def get_next_city(self) -> City:
return self.get_nearest_unchecked_city()

def get_nearest_unchecked_city(self) -> City:
return min(self.unchecked_cities, key=self.get_cost)

def get_cost(self, city: City) -> float:
return self.get_path_distance(city)

def get_path_distance(self, city: City) -> float:
return self.unchecked_cities[city]["distance"]

def get_path_from_city(self, city: City) -> list[City]:
path: list[City] = [city]
while self.checked_cities[city]["previous_city"] != city:
city = self.checked_cities[city]['previous_city']
path.append(city)
path.reverse()
return path

def check_city(self, city: City) -> None:
path_current_total: float = self.get_path_distance(city)
for next_city, cost_to_next_city in self.graph[city].items():
total_cost_to_city: float = path_current_total + cost_to_next_city
if not self.can_be_added_to_queue(
city, next_city, total_cost_to_city
):
continue
self.unchecked_cities.update({
next_city: {
'distance': total_cost_to_city,
"previous_city": city
}
})

def can_be_added_to_queue(
self, current_city: City, next_city: City, cost: float
) -> bool:
if (
next_city in self.checked_cities or
self.unchecked_cities[current_city]["previous_city"] == next_city
):
return False
if (
next_city in self.unchecked_cities and
self.unchecked_cities[next_city]['distance'] < cost
):
return False
return True
36 changes: 36 additions & 0 deletions pathfinder/spfa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pathfinder.city import City
from pathfinder.graphs import Graph
from pathfinder.pathfinder import Pathfinder
from pathfinder.types import Path


class SPFA(Pathfinder):
def __init__(self, graph: Graph):
super().__init__(graph)

def get_next_city(self) -> City:
"""Renvoie en premier dans la file d'attente"""
return list(self.unchecked_cities.keys())[0]

def calculate_shortest_path(self) -> Path | None:
"""Calculez le chemin, il faut passer par chaque ville
pour obtenir le chemin total"""
city_to_check: City = self.start
while bool(self.unchecked_cities):
self.execute_checks(city_to_check)
if bool(self.unchecked_cities):
city_to_check = self.get_next_city()
return self.compute_path(self.end)

def can_be_added_to_queue(self, current_city: City,
next_city: City, cost: float) -> bool:
"""Faites les vérifications de l'algorithme pour voir
si la ville peut être ajoutée à la pile"""
if (next_city in self.checked_cities and
self.checked_cities[next_city]['distance'] > cost):
del self.checked_cities[next_city]
return True
elif next_city in self.checked_cities:
return False
else:
return super().can_be_added_to_queue(current_city, next_city, cost)
Loading