§ 00 · Visual overview

The map is known.
The route is not.

Pathfinding is the cheapest lesson in computer science: the answer is obviously there, and finding it is still hard. Every algorithm below is the same three lines — take a cell off the frontier, look at its neighbours, put them back — and they differ only in which cell you take next. That single choice is the difference between a search that touches four cells and one that touches forty thousand. Everything here runs live in your browser.

settled cells, oldest → newest frontier the path A* returned goal A* searching a cave it has never seen. Click anywhere to move the goal there.
Cells settled
0
Frontier size
0
Map explored
–
Path cost
–
Settled per path cell
–
§ 01 · The frontier

Two algorithms, one line apart

Every search keeps a set of cells it has seen but not yet examined — the frontier, or open set. Take a cell from it, mark it settled, and push its unseen neighbours back on. The entire algorithm is that loop. What you have not specified is which cell to take, and that is not a detail: it is the algorithm. Take the oldest and you get breadth-first search; take the newest and you get depth-first. Same code, same maze, one method call apart.

next = frontier.shift() // oldest first → breadth-first
next = frontier.pop()   // newest first → depth-first

Breadth-first · FIFO queue–

Depth-first · LIFO stack–

settled, in order frontier path returned BFS grows a disc. DFS grows a tendril — it commits to one direction and only backtracks when it hits a dead end.
Map:
BFS settled
0
BFS path length
–
DFS settled
0
DFS path length
–
DFS path excess
–
Peak frontier · BFS / DFS
–

BFS settles cells in strict order of distance from the start, so the first time it touches the goal it has arrived by a shortest route — the path is optimal, and it is optimal for free. The price is that it settles every cell nearer than the goal, whether or not that cell was ever going in the right direction. DFS pays that price too — it settles about as many cells — and gets nothing for it: the route it returns is whatever corridor it happened to fall down, routinely five to ten times longer than necessary. On a perfect maze — no loops, exactly one route between any two cells — that excess collapses to a few percent, because a tree gives DFS nothing to get wrong. Add loops and it comes straight back.

Now look at the peak frontier readout, which usually says the opposite of what people expect. DFS is supposed to be the memory-cheap one — and here its stack runs ten to fifty times larger than BFS's queue. Both claims are correct about different algorithms. The O(depth) figure describes recursive DFS walking a tree, where the only thing on the stack is the current branch. This is an iterative DFS on a graph, and it pushes every unsettled neighbour on discovery, so each expansion adds up to three entries and removes one. The stack grows to a sizeable fraction of the whole map, and the shape on screen is the reason: those long parallel tendrils are all sitting in memory at once, every one of them a branch point waiting to be unwound.

§ 02 · Weights & Dijkstra

Not every step costs the same

Breadth-first search counts steps. The moment a step through a swamp costs five times a step down a road, counting steps returns a route that is short and slow. The fix is to stop taking the oldest cell and start taking the cheapest one — the cell whose known cost-from-start g is smallest. That is Dijkstra's algorithm, and it is BFS with a priority queue instead of a plain one.

settle the open cell with the smallest g   —   g(v) ← min( g(v), g(u) + cost(u→v) )
terrain: cheap → expensive settled, in order cheapest route (Dijkstra) fewest-steps route (BFS) Watch the ripple: it slows to a crawl crossing the marsh and races down the open ground. The wavefront is a contour of equal travel cost.
Fewest steps · cost
–
Cheapest · cost
–
Penalty for counting steps
–
Steps taken · short / cheap
–
Cells settled
0

Drag the marsh cost up and the cheapest route peels away from the straight line and goes the long way round — at some crossover cost the detour becomes worth it, and Dijkstra finds that crossover without being told it exists. Drag it back to 1.0 and the two routes collapse onto each other, because Dijkstra on a uniform-cost graph is exactly BFS. One caveat that matters in practice: this all depends on costs being non-negative. A single negative edge and the whole argument fails — once a cell is settled Dijkstra never revisits it, and a negative edge is precisely a promise that revisiting could have paid.

§ 03 · Heuristics & A*

A good guess is worth a million nodes

Dijkstra spreads in every direction because it knows nothing about where the goal is. But we do know something: on a grid, the straight-line distance to the goal is a free, always-optimistic lower bound on what remains. A* adds it in. Order the frontier not by cost-so-far g but by estimated total trip cost f = g + h, and the search stops spreading like a puddle and starts leaning toward the goal.

f(n) = g(n) cost already paid + w · h(n) optimistic guess of what is left
settled, in order frontier path returned true shortest path — drawn only when A* missed it At w = 0 this is Dijkstra. At w = 1 it is textbook A*. Past w = 1 it is greedy, fast, and no longer trustworthy.
Cells settled
0
vs Dijkstra
–
Path cost
–
Excess over optimal
–
Guarantee
–
The whole trade-off at once — every w from 0 to 3, run to completion on this map
cells settled (left axis) cost of the returned path (right axis) true optimal cost The elbow near w = 1 is the point of the whole section: the first slice of greed is nearly free, the rest is not.

Two things are worth staring at. First, going from w = 0 to w = 1 usually cuts the settled count by a large factor while the path cost curve stays perfectly flat — that speed is free, and it is free because the heuristic never overestimates. Second, past w = 1 the settled count keeps falling but the cost curve lifts off the optimum and starts to wander. You are now buying speed with quality, at an exchange rate you do not control. That is not always a bad trade — a game with a hundred units repathing every frame will happily take a 4% longer route for a 10× cheaper search — but it should be a decision, not an accident.

§ 04 · Admissibility

The one rule a heuristic must not break

A* is optimal under exactly one condition: h must never overestimate the true remaining cost. Guess low and you may waste time; guess high and A* can commit to a route and stop looking, because the alternative looked worse than it was. The map below computes the true cost-to-goal for every single cell with a reverse Dijkstra, then paints coral on every cell where your chosen heuristic claims more than that. Coral is not decoration — it is a map of the lies.

admissible: h(n) ≤ h*(n) for every n   ·   consistent: h(n) ≤ cost(n→m) + h(m) for every edge
cells where h overestimates settled, in order path A* returned true shortest path — drawn only when A* missed it
h:
Moves:
h at the start
–
True cost from start
–
Cells that overestimate
–
Cells settled
–
Path cost
–
Verdict
–

Set 8-way movement with the Manhattan heuristic and the map floods with coral. The reason is arithmetic, not bad luck: to reach a cell ten right and ten up, Manhattan says 20, but ten diagonal moves at √2 each cost 14.1. Manhattan overestimates by up to a factor of √2 — which makes it behave exactly like the weighted A* of § 03 with w = 1.41, complete with a path that is slightly, silently wrong. Switch to octile and the coral disappears entirely: octile is the exact distance on an empty 8-way grid, so it is the tightest guess that still never lies. Switch movement to 4-way and Manhattan becomes exact instead. Admissibility is not a property of a heuristic. It is a property of a heuristic and a movement model, together.

§ 05 · Searching from both ends

Meet in the middle

Here is a saving that costs no heuristic and no accuracy. A uniform search from the start settles roughly everything within distance d — on a plane, an area of about πd². Run two searches instead, one forward from the start and one backward from the goal, and stop when they touch: each only has to reach distance d/2, so together they settle 2 · π(d/2)² = πd²/2. Half the work, same answer. The saving is geometric, and it gets better in higher dimensions.

one disc of radius d  vs  two discs of radius d/2   →   πd² vs πd²/2  ·  in k dimensions, a factor of 2k−1

Dijkstra · one front–

Bidirectional Dijkstra · two fronts–

forward search backward search path returned Both maps solve the same maze and both return a genuinely optimal route — only the settled area differs.
Map:
One front · settled
0
Two fronts · settled
0
Work saved
–
Path cost · one / two
–
Same answer?
–

Try the four maps in order and watch the saving evaporate. On the open field it is close to the predicted half; through rooms it is smaller; on a perfect maze it goes negative — two fronts do more work than one. The disc argument assumed there was area to save, and a maze is a tree: corridors one cell wide, no room to spread, a search that is effectively one-dimensional. In one dimension two searches of length d/2 cover exactly the same ground as one of length d, and the stopping rule then makes each front overshoot slightly past the meeting point. The geometry is the whole saving, so wherever the geometry goes away, so does the benefit.

The other subtlety is knowing when to stop. The two searches meeting is not enough — the first cell they share is rarely on the best route. The correct rule is to keep going until the cheapest cell remaining in the forward queue plus the cheapest remaining in the backward queue is no less than the best joined path found so far; only then is no better join possible. Get that wrong and you have built a fast algorithm that returns nearly-shortest paths, which is a perfectly respectable thing to build as long as you know that is what you did. Combining this with a heuristic is harder still: naïve bidirectional A* can have its two fronts pass each other like ships, and the fixes for it fill papers.

§ 06 · Practice

Now you run the numbers

Five problems, each needing one idea from above and arithmetic you can do on paper. Hints reveal one step at a time — try before you peek, then check yourself against the simulators.

1 · What a perfect heuristic is worth

On an open 4-way grid with no walls, BFS settles every cell within d steps of the start before it reaches a goal at distance d. How many cells is that for d = 100 — and how many would A* settle if its heuristic were exactly right?

Hint 1 — The set of cells at Manhattan distance exactly k from a point is a diamond with 4k cells (for k ≥ 1). Sum that from k = 0 to d.
Hint 2 — 1 + Σ4k = 1 + 4·d(d+1)/2 = 2d² + 2d + 1. With a perfect h, every cell on a shortest path has f = d exactly and every cell off one has f > d, so A* settles only the path.
Answer — BFS settles 2(100²) + 200 + 1 = 20 201 cells; perfect-h A* settles 101. That is a 200× difference, and it grows linearly with d — the gap between knowing nothing and knowing everything is unbounded. Real heuristics live between these two poles, and § 03's settled-cell curve is exactly you sliding along that line. Note also what a perfect heuristic implies: you would already have to know the answer. Heuristic design is the art of knowing almost the answer, cheaply.

2 · Short is not cheap

A courier can cross a marsh — 6 cells at cost 5 each, with 14 ordinary cells at cost 1 on either side of it, for 20 cells total — or take a road detour of 30 cells at cost 1. Which route does BFS return, which does Dijkstra return, and how much does the wrong choice cost?

Hint 1 — BFS minimises the number of cells and cannot see weights at all; Dijkstra minimises the sum of the weights.
Hint 2 — Marsh route: 14 × 1 + 6 × 5 = 44. Road route: 30 × 1 = 30.
Answer — BFS takes the 20-cell marsh route costing 44; Dijkstra takes the 30-cell road costing 30 — BFS's answer is 47% more expensive while looking 33% shorter. Now find the break-even: the routes tie when 14 + 6c = 30, so c = 8/3 ≈ 2.67. Below that the marsh wins, above it the road does, and Dijkstra locates the crossover without being told there is one. This is the § 02 slider, and it is also why a game that stores "difficult terrain" as a boolean will eventually ship a bug.

3 · How wrong can weighted A* be?

You run A* with f = g + 2h and an admissible h, and it returns a route costing 132. What is the worst the true optimum could be — and what is the best it could be? What does that tell you about the route you are holding?

Hint 1 — Weighted A* with weight w and admissible h is w-admissible: the returned cost is at most w times the optimal cost.
Hint 2 — So 132 ≤ 2 · C*, which bounds C* from below. And C* can never exceed what you actually found.
Answer — 66 ≤ C* ≤ 132. The route you are holding is somewhere between exactly optimal and twice as long as necessary, and the algorithm cannot tell you where. That sounds useless until you notice the bound is one-sided and free: any anytime planner can ship the w = 2 answer immediately, then re-run with smaller w while the first answer is already being executed, tightening the interval as it goes. The bound is loose in theory and usually tight in practice — on the § 03 map, w = 2 typically lands within a few percent of optimal, not 100%.

4 · The diagonal trap

An 8-way grid allows diagonal moves at cost √2. Start at (0, 0), goal at (10, 10), no walls. What does the Manhattan heuristic report, what is the true cost, and what does the discrepancy do to A*?

Hint 1 — Manhattan is |Δx| + |Δy| and ignores that a diagonal move covers both at once.
Hint 2 — The true cheapest route is 10 diagonal moves: 10√2 ≈ 14.14. Manhattan reports 10 + 10 = 20.
Answer — Manhattan overestimates by a factor of 20/14.14 = √2 ≈ 1.414, so it is inadmissible here and A* loses its optimality guarantee — it behaves like weighted A* with w = √2, returning routes up to 41% long. The correct heuristic is octile, max(Δx,Δy) + (√2−1)·min(Δx,Δy), which here gives 10 + 0.414 × 10 = 14.14 — exactly right, and never wrong in the other direction either. The lesson generalises past grids: a heuristic is only admissible with respect to a specific set of legal moves, and adding a new move type to your game can silently break a heuristic that was correct yesterday. Watch it happen live in § 04.

5 · Both ends, in three dimensions

§ 05 showed that bidirectional search halves the work on a 2-D map. A drone plans through a 3-D volume, and a voxel graph has branching factor b with the goal at depth d. What is the saving in the branching-factor model, and what does it cost you?

Hint 1 — A uniform search to depth d touches on the order of b^d nodes. Two searches to depth d/2 touch 2·b^(d/2).
Hint 2 — Take b = 6 and d = 20: compare 6²⁰ with 2 × 6¹⁰.
Answer — 6²⁰ ≈ 3.7 × 10¹⁵ versus 2 × 6¹⁰ ≈ 1.2 × 10⁸ — a saving of roughly 30 million times, and in general the exponent is halved: O(b^d) → O(b^(d/2)). The geometric 2^(k−1) figure from § 05 is the same statement for a lattice, which is why the disc argument gives only 2× in 2-D and 4× in 3-D while the tree argument gives an enormous number — the lattice has cycles that collapse most of that branching. What it costs: you must be able to search backwards, which needs the reverse graph. Easy on a symmetric grid, awkward with one-way doors or wind, and impossible when the goal is a description rather than a state — you cannot search backward from "checkmate".