← Explainer Library

Interactive Explainer

Graph Search, Frontier by Frontier

BFS finds the shortest path on an unweighted graph but wastes effort exploring blindly. Dijkstra handles edge weights but ignores the goal. A* adds a heuristic and goes straight there — when the heuristic is admissible. Draw walls on a grid, watch four search algorithms expand their frontiers, and see exactly where each one wastes work.

Prelude

One scaffold, four algorithms

The four classic single-source shortest-path / reachability algorithms — BFS, DFS, Dijkstra, A* — share a single scaffold. Maintain a frontier of nodes you've discovered but not yet finalised. Pop one off, mark it finalised, add its neighbours to the frontier. Repeat until you reach the goal (or the frontier empties).

What changes between them? The data structure backing the frontier.

Different priority order, different behaviour, same underlying loop.

Step 1

Draw, run, watch

Pick a tool, click cells to paint. Walls (black) block movement. Mud (brown) is passable but costs 5× a normal step — only Dijkstra and A* will care. Drag the green start or red goal anywhere. Then press run.

paint:
start goal wall mud (cost 5) frontier explored path
draw walls, then press run.

Things worth trying:

Step 2

BFS — the unweighted shortest-path tool

Pop from a FIFO queue. Each pop either is the goal (return) or pushes its unvisited neighbours. Because the queue is FIFO, every node is popped at its shallowest discovery depth — the number of edges from the start.

BFS is optimal on unweighted graphs (or graphs with uniform edge weights) and runs in $O(V + E)$ time. It is not optimal once edges have different weights: BFS treats a mud cell the same as an empty one. That's the problem Dijkstra fixes.

Step 3

DFS — depth-first, useful for the wrong reasons

Pop from a LIFO stack instead of a queue. You dive as deep as possible before backtracking. DFS is the wrong algorithm for shortest paths — but it's the right one for:

The grid demo above does shortest path, which is the wrong job for DFS — try it and observe the absurd path it finds.

Step 4

Dijkstra — BFS with a priority queue

Replace BFS's FIFO queue with a min-priority queue keyed on the current best-known cost from start. Pop the node with the smallest cost. The first time you pop a node, you have its final shortest-path cost. Update neighbours by relaxation:

With a binary heap (see the trees article) Dijkstra runs in $O((V + E) \log V)$. With a Fibonacci heap, $O(V \log V + E)$, because the decrease-key operation becomes $O(1)$ amortised.

Non-negative edges only. The proof of correctness depends on every edge having non-negative weight. If you have negative edges, Dijkstra is unsafe; use Bellman-Ford ($O(VE)$). If your graph is a DAG, you can do better still: relax in topological order for $O(V + E)$.
Step 5

A* — Dijkstra with a hint

Dijkstra has no notion of where the goal is. A* fixes that: change the priority key from "cost from start" to "estimated total cost through this node":

where $g(n)$ is the actual cost from start to $n$ and $h(n)$ is a heuristic estimate of the remaining cost from $n$ to goal. Three regimes:

For correctness we need $h$ to be admissible — it must never overestimate the true remaining cost. On a 4-connected grid with uniform-cost steps, the Manhattan distance is admissible. On an 8-connected (diagonal) grid, use Chebyshev or octile distance. Euclidean distance is admissible everywhere but loose on grid graphs.

For A* to never re-expand a node we also want $h$ to be consistent (a.k.a. monotone): $h(n) \le c(n, n') + h(n')$ for every edge $(n, n')$. Most "obvious" heuristics on grids — Manhattan, Euclidean — are consistent.

Step 6

Greedy best-first — the cautionary tale

What if you priority-order on just $h(n)$, ignoring the cost paid so far? That's greedy best-first search. It feels like A* with the $g$-term dropped, and it can find a path very quickly on easy maps. But the moment a wall sits between the greedy direction and the goal, it dives into a dead end and slogs back out.

Greedy best-first is not optimal. It produces some path, which may be far from shortest. Useful when (a) you trust the heuristic, (b) you want speed over optimality, and (c) you can re-plan if the path is bad. Otherwise: A*.

Try the C-shaped wall trap above with greedy best-first; watch it bounce off the wall before recovering.

Step 7

The cheat sheet

algorithmfrontierweights?optimal?timetypical use
BFSFIFO queueunit onlyyes (unweighted)$O(V+E)$shortest path on unweighted graphs, breadth-first traversals
DFSLIFO stackany (ignored)no$O(V+E)$topological sort, cycle detection, articulation points
Dijkstramin-heap on $g$non-negativeyes$O((V+E)\log V)$weighted shortest path; routing on roads
Bellman-Fordrelax all edges $V$ timesany (incl. negative)yes$O(VE)$weighted shortest path with negative edges, currency arbitrage
A*min-heap on $g+h$non-negativeyes (admissible $h$)like Dijkstra ×band-widthgame pathfinding, robot navigation, puzzle solving
greedy best-firstmin-heap on $h$anynolike Dijkstrafast approximate routing, anytime planning
Step 8

Sharp edges

Step 9

Reading list