A* Search
Pathfinding with heuristic guidance — combines Dijkstra's optimality with greedy best-first speed.
The Mathematics
Heuristic search, admissibility, and optimality guarantees
Evaluation Function
A* evaluates each node using a function that combines the actual cost from the start with a heuristic estimate to the goal:
: actual cost of the cheapest path from start to
: heuristic estimate of the cost from to the goal
: estimated total cost of the cheapest path through
Admissible Heuristic
A heuristic is admissible if it never overestimates the true cost to the goal:
For grid-based pathfinding, the Manhattan distance is admissible when only 4-directional movement is allowed:
Optimality Proof
If is admissible, A* is optimal. Suppose A* returns a path with cost via goal node. For any unexpanded node on an optimal path:
A* would have expanded before the goal (since ), contradicting that the goal was chosen first. Therefore must be optimal.
A* vs. Dijkstra
Dijkstra is A* with . The heuristic focuses the search toward the goal, expanding fewer nodes. With a perfect heuristic , A* expands only nodes on the optimal path.
See It Work
A* pathfinding on a 5×5 grid with obstacles
Initialize: add start (0,0) to open set. g(start)=0, f(start)=h(start)=8.
The Code
Bridge from heuristic search formulation to Python implementation
Mathematical Formulation
Each node's priority is cost-so-far plus heuristic estimate to goal
Initialize start node with zero cost
Always expand the node with lowest f-score (priority queue min)
If we reached the goal, reconstruct and return the path
Tentative g-score through current node
Only update if we found a shorter path to this neighbor
Manhattan distance heuristic (admissible for grid movement)
Python Implementation
import heapq
def a_star(grid, start, goal):
open_set = [(h(start, goal), start)] # Priority queue: (f_score, node)
came_from = {}
g_score = {start: 0} # Cost from start to node
closed = set()
while open_set:
f, current = heapq.heappop(open_set) # Get lowest f-score
if current == goal:
return reconstruct(came_from, current) # Path found
closed.add(current)
for neighbor in get_neighbors(grid, current):
if neighbor in closed:
continue # Skip already evaluated
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + h(neighbor, goal) # f = g + h
heapq.heappush(open_set, (f, neighbor))
return None # No path exists
def h(node, goal):
return abs(node[0]-goal[0]) + abs(node[1]-goal[1]) # Manhattan