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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Toujours à la recherche de nouveaux exploits à accomplir, vous décidez de par

Commencez par trouver ce qui vous semble être le plus court chemin sans technique particulière. Notez votre réponse.

Bordeaux -> Orléans -> Paris -> Strasbourg : 57h

### 1.b Algorithme de Dijkstra

Sans l'implémenter pour le moment, trouvez le plus court chemin en utilisant manuellement l'algorithme de Dijkstra.
Expand Down
18 changes: 18 additions & 0 deletions __main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from pathfinder.astar import AStar
from pathfinder.city import City
from pathfinder.graphs import graph, spfa_graph
from pathfinder.heuristics import heuristics
from pathfinder.pathfinder import Pathfinder
from pathfinder.spfa import SPFA

pathfinder = Pathfinder(graph)
shortest_path = pathfinder.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
print(shortest_path)

astar = AStar(graph, heuristics)
shortest_path_astar = astar.get_shortest_path(City.BORDEAUX, City.STRASBOURG)
print(shortest_path_astar)

spfa = SPFA(spfa_graph)
shortest_path_spfa = spfa.get_shortest_path(City.NANTES, City.LILLE)
print(shortest_path_spfa)
44 changes: 44 additions & 0 deletions pathfinder/astar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from pathfinder.city import City
from pathfinder.graphs import Graph
from pathfinder.pathfinder import Pathfinder


class AStar(Pathfinder):
"""A* algorithm implementation in Python."""

def __init__(self, graph: Graph, heuristics: dict) -> None:
super().__init__(graph)
self.heuristics = heuristics

def _is_algorithm_done(self, unvisited_nodes: list, current_node: City,
end: City) -> bool:
"""
A* algorithm is done when the current node is the end node.

:param unvisited_nodes: list
:param current_node: City
:param end: City
:return: bool
"""
return current_node != end

def _get_next_node(self, unvisited_nodes: list, current_node: City,
start: City) -> City:

# Heuristics are now taken into account to get the distance to the
# neighbors
unvisited_neighbors = {neighbor: distance + self.heuristics[neighbor]
for neighbor, distance in
self.graph[current_node].items() if
neighbor in unvisited_nodes}

# If unvisited_neighbors is empty, go back to start
if not unvisited_neighbors:
return start

# Return the closest unvisited neighbor
if unvisited_neighbors:
current_node, _ = min(unvisited_neighbors.items(),
key=lambda item: item[1])

return current_node
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"
TOULOUSE = "Toulouse"
PARIS = "Paris"
NANTES = "Nantes"
RENNES = "Rennes"
STRASBOURG = "Strasbourg"
ROUEN = "Rouen"
ORLEANS = "Orléans"
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.MARSEILLE: 18,
City.BORDEAUX: 31,
City.ORLEANS: 20,
City.DIJON: 11,
City.TOULOUSE: 28
},
City.MARSEILLE: {
City.LYON: 18,
City.TOULOUSE: 22
},
City.NANTES: {
City.BORDEAUX: 19,
City.ORLEANS: 16,
City.ROUEN: 19,
City.RENNES: 6
},
City.PARIS: {
City.LILLE: 13,
City.STRASBOURG: 26,
City.DIJON: 16,
City.ORLEANS: 7,
City.ROUEN: 7
},
City.RENNES: {
City.ROUEN: 17,
City.NANTES: 6
},
City.ROUEN: {
City.LILLE: 12,
City.PARIS: 7,
City.ORLEANS: 11,
City.NANTES: 19,
City.RENNES: 17
},
City.STRASBOURG: {
City.LILLE: 29,
City.PARIS: 26,
City.DIJON: 20
},
City.TOULOUSE: {
City.BORDEAUX: 14,
City.LYON: 28,
City.MARSEILLE: 22
},
City.ORLEANS: {
City.BORDEAUX: 24,
City.DIJON: 15,
City.LYON: 20,
City.PARIS: 7,
City.NANTES: 16,
City.ROUEN: 11
}
}

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.NANTES: {
City.ORLEANS: 10,
City.RENNES: 20
},
City.PARIS: {
City.ORLEANS: -30,
City.STRASBOURG: -10,
City.LILLE: 50
},
City.RENNES: {
City.ROUEN: 10,
City.PARIS: 20
},
City.ROUEN: {
City.PARIS: -50
},
City.STRASBOURG: {
City.LILLE: 50
},
City.TOULOUSE: {
City.LYON: -75,
City.MARSEILLE: 40
},
City.ORLEANS: {
City.PARIS: 40,
City.STRASBOURG: 15
}
}
16 changes: 16 additions & 0 deletions pathfinder/heuristics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pathfinder.city import City

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


class Pathfinder:
"""
Base class for subsequent pathfinding algorithms. This class implements
Dijkstra's algorithm.
"""

def __init__(self, graph: Graph) -> None:
self.graph = graph
self.paths_to_nodes = {}

def get_shortest_path(self, start: City, end: City) -> Path:
"""
Method to get the shortest path between two cities using Dijkstra's
algorithm.

:param start: City
:param end: City
:return: Path
"""

# Initialize the paths to all nodes with infinity except the start node
self.paths_to_nodes = {
city: {"total": 0, "steps": [start]} if city == start
else {"total": float("inf"), "steps": [start]}
for city in self.graph
}

# Initialize the unvisited nodes list and remove the start node
unvisited_nodes: list = list(self.graph.keys())
unvisited_nodes.remove(start)

current_node: City = start
while self._is_algorithm_done(unvisited_nodes, current_node, end):
self._update_unvisited_nodes(current_node, start, unvisited_nodes)

# If distance to current node + distance to neighbor is less than
# the total distance to the neighbor, update the total distance to
# the neighbor and the steps to get there
for neighbor, distance in self.graph[current_node].items():
if (self.paths_to_nodes[current_node]["total"] +
distance < self.paths_to_nodes[neighbor]["total"]):
self.paths_to_nodes[neighbor]["total"] = \
self.paths_to_nodes[current_node]["total"] + distance
self.paths_to_nodes[neighbor]["steps"] = \
self.paths_to_nodes[current_node]["steps"] + [neighbor]

current_node = self._get_next_node(unvisited_nodes,
current_node, start)

return self.paths_to_nodes[end]

def _update_unvisited_nodes(self, current_node, start, unvisited_nodes):
"""
Method to update the unvisited nodes list. In Dijkstra's algorithm, the
current node is removed from the unvisited nodes list.

:param current_node: City
:param start: City
:param unvisited_nodes: list
:return: None
"""
if current_node != start:
unvisited_nodes.remove(current_node)

def _is_algorithm_done(self, unvisited_nodes: list, current_node: City,
end: City) -> bool:
"""
Dijkstra's algorithm is done when there are no more unvisited nodes.

:param unvisited_nodes: list
:param current_node: City
:param end: City
:return: bool
"""
return bool(unvisited_nodes)

def _get_next_node(self, unvisited_nodes: list, current_node: City,
start: City) -> City:
"""
Method to get the next node to visit. The next node is the closest
unvisited neighbor.

:param unvisited_nodes:
:param current_node:
:param start:
:return: City
"""

# Create a list of all unvisited neighbors for the current node
unvisited_neighbors = {neighbor: distance for neighbor, distance in
self.graph[current_node].items() if
neighbor in unvisited_nodes}

# If unvisited_neighbors is empty, go back to start
if not unvisited_neighbors:
return start

# Get the closest unvisited neighbor
if unvisited_neighbors:
current_node, _ = min(unvisited_neighbors.items(),
key=lambda item: item[1])

return current_node
Loading