A Bit of History
A* was developed in 1968 by Peter Hart, Nils Nilsson, and Bertram Raphael at the Stanford Research Institute (SRI), as part of the Shakey project — Shakey the Robot, widely considered the first mobile robot capable of reasoning about its own actions, needed a way to plan a path across a room full of obstacles using a limited onboard computer. Their paper, "A Formal Basis for the Heuristic Determination of Minimum Cost Paths," did something unusually rigorous for the era: it didn't just propose a faster heuristic search, it proved exactly which class of heuristics guaranteed the result would still be optimal — the admissibility condition covered below. That formal guarantee is precisely why A* displaced ad-hoc heuristic search methods and became the default choice everywhere from GPS navigation to real-time strategy games.
Working Principle
A* (introduced conceptually in Part 10) is Dijkstra's algorithm with one addition: instead of prioritizing the frontier purely by \(g(v)\) (the actual distance traveled so far), it prioritizes by \(f(v) = g(v) + h(v)\), adding a heuristic estimate \(h(v)\) of the remaining distance to the goal. This single change lets the search "lean toward" the goal instead of expanding uniformly outward.
def astar_pseudocode(graph, start, goal, heuristic):
"""
graph: dict[vertex] -> list[(neighbor, weight)]
heuristic: function(vertex) -> estimated distance to goal
"""
import heapq
g = {start: 0}
open_set = [(heuristic(start), start)] # (f-score, vertex)
came_from = {}
while open_set:
_, current = heapq.heappop(open_set)
if current == goal:
return g[current] # found the optimal cost (if heuristic is admissible)
for neighbor, weight in graph[current]:
tentative_g = g[current] + weight
if neighbor not in g or tentative_g < g[neighbor]:
g[neighbor] = tentative_g
came_from[neighbor] = current
f = tentative_g + heuristic(neighbor)
heapq.heappush(open_set, (f, neighbor))
return None # goal unreachable
Choosing a Heuristic
| Movement model | Heuristic | Formula |
|---|---|---|
| 4-directional grid (no diagonals) | Manhattan distance | \(|x_1-x_2| + |y_1-y_2|\) |
| 8-directional grid (diagonals allowed) | Chebyshev distance | \(\max(|x_1-x_2|, |y_1-y_2|)\) |
| Free movement in any direction | Euclidean distance | \(\sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}\) |
The rule for picking one safely: the heuristic's assumed movement must never be more restrictive than the graph's actual movement rules — Manhattan distance on an 8-directional grid would overestimate distance (diagonal moves make some paths shorter than the heuristic assumes), breaking admissibility.
Worked Example
A 4×1 grid, start at column 0, goal at column 3, using Manhattan distance as the heuristic (here just the horizontal distance remaining).
flowchart LR
S["Start
g=0, h=3, f=3"] --> C1["Cell 1
g=1, h=2, f=3"]
C1 --> C2["Cell 2
g=2, h=1, f=3"]
C2 --> G["Goal
g=3, h=0, f=3"]
Notice \(f\) stays constant at 3 the entire way — exactly the true remaining distance at every step, since the heuristic here is a perfect (if trivial) predictor. In a graph with obstacles, \(f\) would fluctuate as the heuristic's straight-line estimate diverges from the actual best route around barriers, but A* still explores far fewer cells than Dijkstra would, because it never wastes effort expanding cells that clearly lead away from the goal.
Why Admissibility Guarantees Optimality
This is a direct adaptation of Dijkstra's correctness proof (from its deep dive) with the heuristic folded in. Claim: if \(h\) is admissible (never overestimates true remaining distance), the first time A* pops the goal vertex from the priority queue, \(g(\text{goal})\) is the true shortest-path distance. Proof sketch: suppose some other, cheaper path to the goal exists through a currently-unexpanded vertex \(u\). Since \(h\) never overestimates, \(f(u) = g(u) + h(u) \leq g(u) + \text{true remaining distance from } u = \text{true total cost through } u\), which by assumption is less than the goal's current \(f\)-value — so \(u\) (or some vertex on its path) would have been popped from the priority queue before the goal, contradicting that the goal was popped first. This is exactly Dijkstra's argument, generalized to account for the heuristic's optimistic bias.
Complexity Analysis
In the worst case (a completely uninformative heuristic, \(h(v) = 0\) everywhere), A* degrades exactly to Dijkstra's algorithm:
$$\text{Time: } O((V+E)\log V) \text{ worst case} \qquad \text{Typically far less with a strong heuristic}$$
The practical speedup from a good heuristic is often dramatic — orders of magnitude fewer vertices explored on large spatial graphs — even though the worst-case bound doesn't change.
Implementation
Real-World Applications
Video Game Pathfinding and Robot Navigation
Nearly every real-time strategy game, MOBA, and open-world title uses A* (often over a simplified "navigation mesh" rather than a raw pixel grid) to move units and NPCs around obstacles in real time — its predictable, tunable behavior via heuristic weighting makes it far more practical for games than exhaustive search. The same core algorithm, in continuous-space variants, still guides mobile robots and self-driving vehicle motion planners today — a direct, unbroken lineage back to Shakey navigating a 1960s SRI laboratory.
Exercises
- Run A* by hand on a small grid with one obstacle, using Manhattan distance, and count how many cells it expands compared to a plain BFS/Dijkstra sweep.
- Prove that Euclidean distance is always admissible for a graph where movement is allowed in any direction at unit cost per unit distance.
- Construct a heuristic that overestimates in at least one case, and show a concrete example where A* using it returns a suboptimal path.
- Challenge: Implement A* with a "weighted" heuristic \(f(v) = g(v) + w \cdot h(v)\) for \(w > 1\) (deliberately sacrificing optimality for speed), and measure the tradeoff between path quality and vertices expanded as \(w\) increases.
Limitations
Only As Good As Its Heuristic
A* offers no benefit over Dijkstra without a well-chosen, domain-specific heuristic — a poor or non-admissible heuristic can make it slower than plain Dijkstra (extra bookkeeping for no gain) or, worse, silently wrong. It also stores every expanded vertex's data in memory, which becomes a genuine constraint on enormous search spaces — memory-bounded variants like IDA* (Iterative-Deepening A*) trade some speed for a much smaller memory footprint in exactly those cases.