Skip to content

Commit 4657dea

Browse files
committed
Updated the README
1 parent 67e7c4a commit 4657dea

File tree

1 file changed

+155
-1
lines changed

1 file changed

+155
-1
lines changed

README.md

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,162 @@ This is different because I refactored all the logic in propositional layers. Th
1212

1313
# How can it be TESTED?
1414

15-
It comes with a `test.py` snippet to test the algorithm in a simple weighted maze.
15+
It comes with a `test_a_star.py` snippet to test the algorithm in a simple weighted maze ( with random weights ). If you run it you will obtain something like this:
16+
17+
<p align="center">
18+
<img src="./doc/sample.png">
19+
</p>
1620

1721
# Can you EXPLAIN the algorithm?
1822

23+
Yeah! The idea of the A* algorithm is that starting from the start point we visit the points that are cheaper to visit. The cost of visiting a neighbor point depends on how costly is to go from the current point to a neighbor. So we check for all the points what is the neighbor that is cheaper to visit and we visit it.
24+
25+
The A* algorithm is basically the following:
26+
27+
```python
28+
def a_star(graph, start, end):
29+
"""
30+
Calculates the shortest path from start to end.
31+
32+
:param graph: A graph object. The graph object can be anything that implements the following methods:
33+
34+
graph.neighbors( (x:int, y:int) ) : Iterable( (x:int,y:int), (x:int,y:int), ...)
35+
graph.cost( (x:int,y:int) ) : int
36+
37+
:param start: Tuple of two ints representing the starting point.
38+
:param end: Tuple of two ints representing the ending point.
39+
:returns: A DijkstraHeap object.
40+
41+
"""
42+
43+
frontier = DijkstraHeap( Node(0, start, None) )
44+
45+
while frontier:
46+
47+
current_node = frontier.pop()
48+
49+
if current_node.value == end:
50+
return frontier
51+
52+
for neighbor in graph.neighbors( current_node.value ):
53+
54+
new_cost = ( current_node.priority
55+
+ graph.cost(current_node.value, neighbor)
56+
+ heuristic( neighbor, end) )
57+
58+
new_node = Node(new_cost, neighbor, current_node.value)
59+
60+
frontier.insert(new_node)
61+
62+
```
63+
64+
Lets go line by line:
65+
66+
```python
67+
frontier = DijkstraHeap( Node(0, start, None) )
68+
```
69+
70+
This line creates a DijkstraHeap object. We will see later how this can be implemented but the best part is that....This is not part of the algorithm! What is a DijkstraHeap then? This is a **priority queue** that has the following properties:
71+
72+
* If we try to insert an already visited element in the queue the DijkstraHeap will do nothing.
73+
* The DijkstraHeap always pop the element that has the lowest cost and NEVER pops an already visited element.
74+
75+
Cool! So this DijkstraHeap knows the visiting order of the elements. Its **like a heap but never pops an already visited element**.
76+
77+
```python
78+
while frontier:
79+
```
80+
81+
We loop while we have elements in the queue.
82+
83+
```python
84+
current_node = frontier.pop()
85+
```
86+
87+
Each iteration we pop an element from the DijkstraHeap. This element always has the lowest cost element because the DijkstraHeap has this property ( because is a heap and heaps are awesome ).
88+
89+
```python
90+
if current_node.value == end:
91+
return frontier
92+
```
93+
94+
If we have reached the end, we stop and return the DijkstraHeap that has all the information about our path (because it knows how we reach each element).
95+
96+
```python
97+
for neighbor in graph.neighbors( current_node.value ):
98+
```
99+
100+
We get each of the current point neighbors
101+
102+
```python
103+
new_cost = ( current_node.priority
104+
+ graph.cost(current_node.value, neighbor)
105+
+ heuristic( neighbor, end) )
106+
107+
new_node = Node(new_cost, neighbor, current_node.value)
108+
109+
frontier.insert(new_node)
110+
111+
```
112+
113+
For each neighbor we calculate the new cost of reaching this neighbor from the current point. This cost is formed by three quantities:
114+
115+
1. The current cost of reaching the current point.
116+
2. The cost of going from the current point to the neighbor.
117+
3. The distance of the neighbor to the end point that we are looking.
118+
119+
Why this 3rd cost? Because we want to explore first the points that are near the end destination and expend less time in the points that are far from it. So if we artificially give the point a higher cost if the point is far from the destination it will be visited later.
120+
121+
When we have calculated this new cost we insert the point in the priority queue.
122+
123+
## But what about the MISTERIOUS DijkstraHeap?
124+
125+
Is like I said a heap that remembers the visited elements and where they came from and never pops an already visited element. The implementation is very simple:
126+
127+
```python
128+
class DijkstraHeap(list):
129+
"""
130+
An augmented heap for the A* algorithm. This class encapsulated the residual logic of
131+
the A* algorithm like for example how to manage elements already visited that remain
132+
in the heap, elements already visited that are not in the heap and from where we came to
133+
a visited element.
134+
135+
This class will have three main elements:
136+
137+
- A heap that will act as a priority queue (self).
138+
- A visited dict that will act as a visited set and as a mapping of the form point:came_from
139+
- A costs dict that will act as a mapping of the form point:cost_so_far
140+
"""
141+
def __init__(self, first_node = None):
142+
self.visited = dict()
143+
self.costs = dict()
144+
145+
if first_node is not None:
146+
self.insert(first_node)
147+
148+
def insert(self, element):
149+
"""
150+
Insert an element into the Dijkstra Heap.
151+
152+
:param element: A Node object.
153+
:return: None
154+
"""
155+
156+
if element.value not in self.visited:
157+
heapq.heappush(self,element)
158+
159+
def pop(self):
160+
"""
161+
Pop an element from the Dijkstra Heap, adding it to the visited and cost dicts.
162+
163+
:return: A Node object
164+
"""
165+
166+
while self and self[0].value in self.visited:
167+
heapq.heappop(self)
19168

169+
next_elem = heapq.heappop(self)
170+
self.visited[next_elem.value] = next_elem.came_from
171+
self.costs[next_elem.value] = next_elem.priority
172+
return next_elem
173+
```

0 commit comments

Comments
 (0)