Understanding the A* Search Algorithm for Pathfinding
Pathfinding is the task of finding a route between locations in a graph. A location might be a square on a game map, a street intersection, a warehouse shelf, or a state in a puzzle. The A* search algorithm is one of the most useful methods for this job because it combines the reliability of Dijkstra’s algorithm with a heuristic that guides the search towards the destination.
A* appears in games, robotics, mapping software, network routing and many programming problems. Its performance depends on how the graph is represented, how neighbours are generated, and whether the heuristic accurately estimates the remaining distance. For readers building their fundamentals, the algorithms library offers useful context around graph traversal and related techniques.
How A* Chooses A Route
A* assigns every discovered node a score called f(n):
f(n) = g(n) + h(n)
Here, g(n) is the known cost of travelling from the start node to n, while h(n) is the estimated cost from n to the goal. The algorithm always expands the node with the smallest f value. This balances two priorities: staying on a reasonably cheap route and moving in a promising direction.
Dijkstra’s algorithm uses only the cost already paid, so its priority is effectively g(n). A greedy best-first search uses only the estimate, h(n), and can rush towards the goal along an expensive route. A* combines both values, allowing it to be directed without giving up shortest-path guarantees.
Imagine navigating from Flinders Street Station to Carlton in Melbourne on a grid-like map. A* considers both the distance already travelled through the city and the estimated distance still remaining. Barriers, one-way streets and different road costs can be represented in the graph, so the final route need not be a straight line.
The Graph And Search Frontier
A pathfinding problem begins with a graph. Nodes represent positions or states, and edges represent legal moves between them. Each edge has a cost, such as one movement point, a road distance, travel time or energy consumption. In a tiled game map, a node may have four neighbours for north, south, east and west movement, or eight if diagonal movement is allowed.
A* usually maintains two important collections. The open set contains nodes that have been discovered but not fully explored. The closed set, or an equivalent visited structure, records nodes that have already been processed. A priority queue is commonly used for the open set because it efficiently returns the node with the smallest estimated total cost.
The basic process is:
put the start node in the priority queue
set its g value to 0
while the queue is not empty:
remove the node with the smallest f value
if it is the goal:
reconstruct and return the path
for each valid neighbour:
calculate a new g value
if this route is cheaper:
update the neighbour
record its parent
add it to the queue
return failure if no route exists
When a cheaper route to a neighbour is found, the algorithm updates that node’s parent. Following parent references backwards from the goal eventually reconstructs the path from start to destination. In Python, a dictionary can store each node’s parent and best-known cost, while heapq can manage the priority queue.
Designing A Useful Heuristic
The heuristic is the estimate h(n). It should be quick to calculate and should reflect the movement rules of the graph. For a four-direction grid where every move costs one, Manhattan distance is usually suitable:
h(n) = abs(n.x - goal.x) + abs(n.y - goal.y)
For a grid allowing diagonal movement with equal costs, Chebyshev distance is often appropriate. For a map with straight and diagonal movement at different prices, an octile-distance formula gives a better estimate. In a road network, a straight-line geographic distance can be useful when roads cannot provide a shorter travel time than direct movement.
A heuristic is called admissible when it never overestimates the true cheapest remaining cost. An admissible heuristic helps A* find an optimal path, assuming edge costs are non-negative. A consistent, or monotonic, heuristic also obeys a triangle-like relationship between neighbouring nodes. Consistency reduces the need to revisit nodes and makes implementation easier.
A heuristic that always returns zero is admissible, but it turns A* into Dijkstra’s algorithm and provides no directional guidance. An aggressive heuristic may explore fewer nodes, yet if it overestimates, the returned route can be suboptimal. Testing should therefore compare the result with a trusted baseline on small maps.
Implementing A* Efficiently
A simple implementation can use a min-heap containing pairs such as (f_score, node). Since a heap may contain outdated entries after a node receives a better score, the algorithm can discard an entry when its stored priority no longer matches the current best value. This approach is often easier than trying to update an item inside the heap.
For grid-based pathfinding, the following details have a large effect on behaviour:
- Store blocked cells in a fast set or boolean matrix.
- Decide whether diagonal movement may pass between two blocked corners.
- Keep movement costs consistent with the heuristic.
- Record parent nodes only when a cheaper route is discovered.
- Return an empty path or a clear failure value when the goal is unreachable.
Memory usage can become the main limitation on a large map. The open set, closed set, parent map and cost map may all contain many nodes. A compact two-dimensional array works well for dense grids, while dictionaries are more flexible for sparse maps. If the world is enormous, hierarchical pathfinding or a navigation mesh may reduce the search area before A* begins.
For a road or transport graph, nodes can represent intersections and edges can store travel times. A trip around Sydney may involve toll roads, traffic-weighted edges and restricted turns, so geographic distance alone does not describe the real cost. The graph model must capture the rules that matter to the application.
Correctness, Complexity And Testing
With a suitable admissible heuristic, A* returns a lowest-cost path. If the heuristic is consistent, a node generally does not need to be reopened after it has been removed from the priority queue. These guarantees assume that edge costs are non-negative and that the graph representation correctly describes all legal moves.
The exact time complexity depends on the graph and priority-queue implementation. A common theoretical description is O((V + E) log V) when a binary heap is used, where V is the number of vertices and E is the number of edges. In practical grid searches, the number of expanded cells is often more informative than the worst-case expression. A strong heuristic can dramatically reduce expansions, while a poor one makes A* behave closer to uniform-cost search.
Testing should cover simple routes, blocked goals, unreachable regions, ties between equally short paths and varied movement costs. Include maps with narrow corridors and obstacles that tempt the search in the wrong direction. For algorithm practice, the problem-solving guide provides a useful way to think about constraints, edge cases and complexity before writing code.
A good diagnostic is to count expanded nodes and compare several heuristics. If Manhattan distance is used on a four-way grid, verify that diagonal moves have not accidentally been enabled. If the goal is reached but the reconstructed path contains a jump between non-adjacent cells, the parent-tracking logic is incorrect.
Practical Pitfalls To Avoid
Many bugs come from mixing coordinate conventions. A program may store positions as (row, column) but calculate distance as if they were (x, y). The result can still look plausible on symmetrical maps, which makes this error particularly difficult to notice. Define one convention and apply it to neighbours, distance calculations and output.
Another common issue is using a heuristic that does not match movement costs. If horizontal movement costs five but the heuristic assumes every move costs one, the estimate may be too weak. That does not usually break correctness, but it can cause unnecessary exploration. If the heuristic overestimates instead, shortest-path guarantees may disappear.
Useful implementation checks include:
- Reject a blocked start or goal before searching.
- Verify that every generated neighbour is inside the map.
- Use a stable tie-breaking rule when two nodes have equal scores.
- Prevent diagonal corner-cutting when the application requires realistic movement.
- Confirm that the returned path begins at the start and ends at the goal.
A* can also be misused when the environment changes frequently. If a road closes or a game obstacle moves, an old route may become invalid. Re-running A* is acceptable for small maps, but dynamic systems may benefit from incremental methods such as D* or Lifelong Planning A*. The right choice depends on how often the graph changes and how expensive a fresh search would be.
The algorithm can be extended with a secondary tie-breaker. For example, among nodes with equal f values, the implementation might prefer the one with the larger g value. This can make routes look more direct in some grids, but tie-breaking changes search behaviour rather than the fundamental cost model.
Where A* Works In The Real World
In games, A* can guide a character through rooms, terrain and moving obstacles. A map near Perth might contain huge open areas connected by narrow traversable zones, while a Sydney bush track could be modelled as a network of walkable segments with steep or hazardous sections assigned higher costs. The algorithm does not need to know the map is Australian; it simply follows the graph and cost rules supplied by the program.
Robotics uses similar ideas for warehouse vehicles, drones and autonomous machines. A robot can treat dangerous surfaces, sharp turns or battery-intensive movement as expensive edges. For a delivery robot working around a busy shopping centre, the cheapest route may be slower in metres but safer in terms of collisions and restricted areas.
Transport applications often use weighted graphs where the cost is time rather than distance. A route from Adelaide to a regional town may involve long stretches between services, while a city trip may include congestion, tolls and changing traffic conditions. A* can support such systems when the heuristic remains a safe lower bound on the actual remaining travel time.
For everyday coding, A* is a strong example of how data structures and problem-solving ideas work together. The priority queue selects the next candidate, maps store costs and predecessors, and the heuristic adds domain knowledge. Understanding those interactions makes it easier to recognise when breadth-first search, Dijkstra’s algorithm or another graph method would be a better fit. Grab a cuppa, sketch the state space, define the costs carefully and then let the search explore only the most promising parts of the graph.