The A* (pronounced “A star”) search algorithm is one of the most popular and effective pathfinding algorithms used in computer science and artificial intelligence. It is an informed search method that aims to find the shortest path between a starting node and a goal node in a graph or grid by exploiting heuristics to guide its search.
In this comprehensive guide, we will learn how to implement the A* algorithm in Python step-by-step, with example code snippets and detailed explanations.
Table of Contents
Open Table of Contents
Overview of the A* Algorithm
The A* algorithm is best suited for pathfinding problems in graphs and grids, where you need to find the shortest path between two points. It combines features of both uniform-cost search and pure heuristic search to efficiently compute optimal solutions.
Some key properties of A*:
- Informed search - Uses heuristics to guide search towards most promising nodes
- Optimality - Guarantees shortest path if heuristic is admissible
- Completeness - Guaranteed to find a solution if one exists
- Efficiency - Finds solutions quickly by exploiting heuristic information
At a high level, A* algorithm works as follows:
- Initialize open and closed list of nodes
- Set starting node as current node
- Loop until solution found:
- Consider current node’s neighbors
- Add neighbors to open list
- Set f-cost for each neighbor (g+h)
- Sort open list by lowest f-cost
- Set lowest f-cost node as current node
- Move current node to closed list
- Reconstruct path by traversing pointers backwards from goal
Where:
- g = cost to move from starting point to current node
- h = estimated cost to move from current node to goal (heuristic)
- f = total estimated cost of path through node (f=g+h)
By leveraging heuristics, A* can find optimal solutions faster than brute force methods.
Implementing the A* Algorithm in Python
We will implement a basic version of the A* algorithm for pathfinding in a 2D grid/graph.
Our implementation will have the following key components:
Node
class to represent each node on gridAStar
class with the main search algorithm- Helper functions for heuristics, reconstructing path, etc.
Let’s go through each section step-by-step:
The Node Class
First, we’ll create a Node
class to represent each node on our 2D grid map. Each node will store its position on the grid, its neighbors, and other useful data.
class Node:
def __init__(self, pos: tuple, g_cost: float, h_cost: float):
self.pos = pos # (x, y) tuple
self.g_cost = g_cost
self.h_cost = h_cost
self.f_cost = self.g_cost + self.h_cost
self.parent = None #previous node
The __init__()
constructor takes a position tuple, g_cost
and h_cost
values. It calculates the f_cost
and sets the initial parent
to None.
We’ll also add comparator methods to the Node class so we can compare and sort nodes efficiently:
def __lt__(self, other):
return self.f_cost < other.f_cost
This __lt__()
method allows us to compare two nodes using their f_cost
values.
A* Search Algorithm
Next, we’ll implement the main A* search logic inside an AStar
class:
class AStar:
def __init__(self, map_grid):
self.open = [] #open list
self.closed = [] #closed list
self.map_grid = map_grid
The __init__()
method takes the grid map as input and initializes the open and closed lists to empty.
def search(self, start_node, goal_node):
self.open.append(start_node)
while self.open:
#sort open list to get node with lowest cost first
self.open.sort()
current_node = self.open.pop(0)
#add current node to closed list
self.closed.append(current_node)
if current_node == goal_node:
#reached goal node
return self.reconstruct_path(goal_node)
#check every neighbor
neighbors = self.get_neighbors(current_node)
for neighbor in neighbors:
if neighbor in self.closed:
continue
g_cost = current_node.g_cost + 1 #cost to move
h_cost = self.heuristic(neighbor, goal_node)
f_cost = g_cost + h_cost
#check if we found cheaper path
if neighbor in self.open:
if neighbor.f_cost > f_cost:
self.update_node(neighbor, g_cost, h_cost)
else:
self.update_node(neighbor, g_cost, h_cost)
#no path found
return None
The main search()
method takes the start and goal nodes as input. We add the start node to the open list to begin.
It then loops while there are nodes in the open list, sorting it to get the lowest f-cost node each iteration.
We pop the node off and add it to closed. Check if it is our goal, return the path if so.
Otherwise, look at the current node’s neighbors. For each valid new neighbor, calculate costs and update its values if we found a lower cost path.
Next we need to implement the helper methods called by search()
:
def get_neighbors(self, node):
pass #calculate valid adjacent nodes
def heuristic(self, node, goal):
pass #estimate cost to goal
def reconstruct_path(self, goal_node):
pass #follow parents back to start
def update_node(self, node, g_cost, h_cost):
pass #update if we find better path
Helper Functions
Let’s implement the helper methods:
def get_neighbors(self, node):
dirs = [[1,0], [0,1], [-1,0], [0,-1]]
neighbors = []
for dir in dirs:
neighbor_pos = (node.pos[0] + dir[0], node.pos[1] + dir[1])
#check if new pos in bounds
if (0 <= neighbor_pos[0] < self.map_grid.shape[0] and
0 <= neighbor_pos[1] < self.map_grid.shape[1]):
#check if traversable
if self.map_grid[neighbor_pos] != 1:
neighbors.append(neighbor_pos)
return neighbors
get_neighbors()
calculates the adjacent nodes, ensuring they are within grid bounds and not blocked.
For the heuristic, we’ll use the Manhattan distance, which is the absolute grid distance between two points:
def heuristic(self, node, goal):
d = abs(node.pos[0] - goal.pos[0]) + abs(node.pos[1] - goal.pos[1])
return d
To reconstruct the path after finding the goal node:
def reconstruct_path(self, goal_node):
path = [goal_node]
current = goal_node
while current.parent != start_node:
path.append(current.parent)
current = current.parent
return path[::-1] #reverse path
We follow parent nodes backwards until the start, then reverse the list.
Finally, to update a node if we find a better path:
def update_node(self, node, g_cost, h_cost):
node.g_cost = g_cost
node.h_cost = h_cost
node.f_cost = g_cost + h_cost
node.parent = current_node
This updates the node’s cost values if the new f-cost is lower.
And that covers the core A* algorithm implementation in Python!
Example Usage
Let’s look at a simple example of using our AStar
class to find a path on a small grid:
grid = np.array([
[0,0,0,0,1],
[0,0,0,0,0],
[0,0,1,0,0],
[0,0,1,0,0],
[0,0,0,0,0]
])
start = Node((0,0))
goal = Node((len(grid)-1, len(grid[0])-1))
astar = AStar(grid)
path = astar.search(start, goal)
We create a 5x5 grid with some obstacles, define the start and end nodes, instantiate the AStar
class, and call search()
to find the shortest path from start to goal while avoiding obstacles.
The returned path
contains a list of node positions that form the optimal path. We can render this visually on the grid map.
This demonstrates how to apply the A* algorithm to grid pathfinding problems in a simple Python implementation.
Optimizations and Improvements
There are many ways we can optimize or extend our basic A* implementation:
- Use a priority queue for open list - More efficient than sorting entire list each time
- Track visited nodes with a set - Faster lookup than searching in closed list
- Use better heuristics - e.g. Octile, Chebyshev for more accurate cost estimates
- Bidirectional search - Search from goal backwards to reduce nodes explored
- Iterative deepening - Incrementally increase search depth to control memory use
- JPS (Jump Point Search) - Optimization for uniform-cost grids
- Lifelong Planning A* - Reuse previous search efforts for faster replanning
Applying these optimizations can significantly improve A* performance on larger graphs and complex grids.
Some other ideas for improvements:
- Support diagonal movement
- Allow weighted edges with different costs
- Add tie-breaking logic if multiple nodes have equal low f-cost
- Handle dynamic graphs/grids where edges can change
- Animate the search process step-by-step
There is substantial scope to tailor the A* implementation to your specific use case. The algorithm is very versatile.
Applications of A* Search
The A* algorithm excels at finding shortest paths in graphs and grids. This makes it applicable to a wide range of problems:
Pathfinding - Navigation in video games, maps, robotics
Travel routing - Find fastest route in road networks, public transit
Wire routing - Laying out circuit designs
Board games - Chess, checkers, puzzles like the 15-puzzle
Network routing - Optimizing packet routing in networks
Machine learning - Heuristic search in decision trees, neural networks
Bioinformatics - Alignment of protein or gene sequences
Robotics - Motion planning algorithms for robots
A* combines heuristic guidance with guaranteed optimality, making it a go-to approach for shortest path problems across many domains.
Summary
In this guide, we learned how to implement the powerful A* pathfinding algorithm in Python through an object-oriented approach:
- Discussed how A* search leverages heuristics to efficiently find optimal paths
- Built a
Node
class to represent grid points - Implemented the core A* logic inside an
AStar
class - Added helper functions for calculating neighbors, heuristics and reconstructing paths
- Demonstrated simple usage of the A* class to find paths
- Explored some ideas for optimizing and improving the algorithm
- Overviewed real-world applications of A* in pathfinding, routing, and more
The A* algorithm is widely used for finding shortest paths due to its excellent performance. This step-by-step Python implementation guide provides a solid foundation for applying A* to pathfinding problems in any graph-based domain.