Skip to the lesson
FixTheRobot Explore Robotics / Navigation / M3 Global Planning: Graph Search / M3-T2

Navigation · Theory · M3

M3-T2 — Breadth-First Search and Dijkstra

Two ways to explore a map outwards from the start, and why only one of them knows about cost.

  • Lesson 2 of 7
  • Module 3: Global Planning: Graph Search
  • Theory
  • About 8 minutes to read

Every grid planner in this module is a variation of one idea: keep a frontier of places you have reached but not yet explored, and always explore the most promising one next. Breadth-first search picks the oldest, Dijkstra picks the cheapest. That single change is the difference between the route with the fewest steps, through the crowded waiting room, and the route with the lowest cost, round it.

Learning objectives

After this lesson, you should be able to:

  • Run breadth-first search and Dijkstra by hand on a small graph and record the queue.
  • State why a priority queue gives shortest paths when step costs are not negative.
  • Explain why a node is final when it is popped and not when it is first reached.
  • Give the running time of each method and say what a negative cost breaks.

The intuition

Picture a drop of ink spreading through the corridors of the hospital. After one step it has reached the neighbouring cells, after two steps their neighbours, and so on. If every step takes the same time, the ink reaches each cell exactly when the shortest route to it would. That is breadth-first search: it expands in rings of equal step count.

Now suppose the ink moves faster over polished floor and slower through the crowded waiting room. The first place the ink arrives at is no longer the nearest by steps but the nearest by time. Dijkstra's algorithm is the same ink with the clock running: it always expands the place the ink would reach next. The result, for every place, is the time at which the ink first arrives and the neighbour it came from, and following those neighbours back from the goal is the cheapest route.

A six-place graph with Dijkstra distancesPlaces S, A, B, C, D, G joined by edges with costs: S to A 4, S to B 1, B to A 2, A to C 5, B to C 8, C to G 3, A to D 10, D to G 2. The cheapest route from S to G is S, B, A, C, G, with cost 11, drawn thick. The route with the fewest edges, S, A, C, G, costs 12, drawn dashed.412583102Sd = 0Bd = 1Ad = 3Cd = 8Dd = 13Gd = 11thick: cheapest route (cost 11, 4 edges) dashed: fewest edges (cost 12, 3 edges)
The route with fewest edges (dashed) is not the cheapest. Distances d are the cheapest costs from S. D is explored last and never matters for G.

The mathematics

Breadth-first search

Keep a first-in first-out queue. Take the oldest cell, add every unvisited neighbour to the back, and remember where each came from. Each cell enters the queue once, so the time is proportional to the cells and edges.

BFS(start):
  queue ← [start];  seen ← {start}
  while queue not empty:
    u ← pop front
    for each neighbour v of u not in seen:
      seen ← seen ∪ {v};  parent[v] ← u;  push v at the back

When every step costs the same, BFS finds a path with the fewest steps, and so the cheapest. When costs differ it does not.

Dijkstra's algorithm

Keep for every node a tentative cost d, the best cost found so far, and a priority queue ordered by d. Repeatedly take the node with the smallest d, declare it final, and relax its edges: if going through it gives a neighbour a lower cost, update that cost and parent.

Dijkstra(start):
  d[start] ← 0;  d[other] ← ∞;  queue ← {(0, start)}
  while queue not empty:
    (du, u) ← pop smallest
    if du > d[u]: continue            # a stale entry
    if u is the goal: stop
    for each edge (u, v) with cost c ≥ 0:
      if d[u] + c < d[v]:
        d[v] ← d[u] + c;  parent[v] ← u;  push (d[v], v)
d[v]
the best known cost from the start to v. It only ever decreases, and it is exact once v has been popped.
parent[v]
the node from which d[v] was achieved. Following parents back from the goal gives the route.
stale entry
a queue entry whose cost is higher than the current d[v]: the node was found again more cheaply. It is skipped rather than removed (lazy deletion).

Why it works

The key fact is an invariant: when a node u is popped, d[u] is the true shortest cost. Suppose not. Then the true shortest path to u leaves the set of finished nodes through some edge from a finished node x to an unfinished node y, with y still in the queue. Because d[y] ≤ d[x] + c(x, y) was set when x was finished, and costs are not negative, d[y] is no more than the true cost to u, and so it is no more than d[u]. But u was popped instead of y, so d[u] ≤ d[y]. Hence d[u] equals the true cost. The argument breaks if any c is negative, because the path beyond y could then be cheaper than y.

Running time

Search methods compared
MethodQueueTimeFinds
Breadth-firstFIFO listO(V + E)fewest steps
Dijkstra, binary heappriority queueO((V + E) log V)lowest cost, costs ≥ 0
Dijkstra, scan for minimumarrayO(V²)same; used in small dense graphs

For a grid, E is about 2 to 4 times V, so Dijkstra with a heap is about O(V log V). Searching the whole floor of lesson M3-T1 (240,000 cells) takes a fraction of a second; with a goal in view, the search stops when the goal is popped, which is usually much earlier.

A worked example

Six places. Scout is at S and the pharmacy is G. The graph is the one in the figure. Costs: S–A 4, S–B 1, B–A 2, A–C 5, B–C 8, C–G 3, A–D 10, D–G 2.

pop S (0)    relax A = 4, B = 1
pop B (1)    relax A = 3 (was 4), C = 9
pop A (3)    relax C = 8 (was 9), D = 13
  skip stale A (4)
pop C (8)    relax G = 11
  skip stale C (9)
pop G (11)   goal reached: stop

Follow the parents back from G: G ← C ← A ← B ← S. The route S, B, A, C, G costs 1 + 2 + 5 + 3 = 11. Two things are worth noticing. First, A was reached first by the direct edge from S at cost 4, and a moment later through B at cost 3: marking A final when it was first discovered would have given the wrong answer. Second, D, with d = 13, was never expanded, because G was popped before it.

Breadth-first on the same graph. BFS reaches G in three edges, S, A, C, G, at cost 4 + 5 + 3 = 12. It is the route with the fewest edges and it is one unit worse than the cheapest, because it never looked at costs. On a hospital grid the same effect is a route through the crowded waiting room: fewest steps, highest cost (lab M3-L1).

The cost of a lazy queue. The trace pushed 8 entries (S, A, B, A, C, C, D, G) and popped 5 nodes plus 2 stale entries, leaving D unexpanded. With lazy deletion the queue can hold up to E entries instead of V, and that is a good trade: no heap needs a “decrease key” operation.

Engineering implications and limitations

Stop at the goal

For a single goal, stop when it is popped. Stopping when it is first pushed returns a route that may be too expensive; the goal's cost can still fall while it waits in the queue (lab M3-L1, stage 2).

One search, many answers

A search from the start gives the cost to every cell, so the nearest of several goals comes from one search that stops at the first goal popped. Run backwards from the goal it gives a cost-to-go field for every possible robot position, which the replanning lessons build on (lesson M3-T6).

Ties

Many routes can have the same cost. Which one the search returns depends on how ties are broken in the queue, so tests should compare costs and validity, not exact routes.

When Dijkstra is the right tool

Dijkstra has no heuristic, so it spreads evenly in every direction. That is wasteful for one goal on a large map, and ideal when you need distances to many places, for example a cost map from a charging dock, a distance field, or a map of how far every room is from the pharmacy. For a single goal, A* (lesson M3-T3) does the same work with fewer expansions.

Floating point costs

With diagonal costs of √2 the totals are not exact. Compare costs with a tolerance, and do not use floating point equality to decide whether a cell has improved by a tiny amount: use a strict improvement test.

Common misconceptions

“The first time a node is reached is the cheapest.”

Only for breadth-first search with equal step costs. With different costs a node can be reached cheaply later; Dijkstra finalises a node when it is popped, not when it is pushed.

“Dijkstra needs the heap to be updated when a cost drops.”

Not necessarily: pushing a second entry and skipping the stale one on popping is simpler and just as correct.

“A negative cost is fine if there is no negative cycle.”

The Bellman-Ford method handles that case. Dijkstra’s algorithm does not: its invariant needs costs of zero or more, and a single negative edge can give a wrong answer.

“Breadth-first search is just a slow Dijkstra.”

It is a different algorithm that minimises a different thing, the number of steps, and it is faster (no heap) when steps are what matters.

Engineering takeaways

  1. Breadth-first search expands in rings of equal step count and minimises steps; Dijkstra expands in order of cost and minimises cost.
  2. Dijkstra keeps a best-known cost and parent per node, and relaxes edges of the node it pops.
  3. A node is final when it is popped; this holds only if no step cost is negative.
  4. With a binary heap Dijkstra runs in O((V + E) log V); stopping at the goal usually saves most of it.
  5. One search from the start yields costs to all nodes, and one backwards from the goal yields a cost-to-go for every start.

References and further reading

The original paper and the standard textbook treatments.

  • E. W. Dijkstra, “A note on two problems in connexion with graphs”, Numerische Mathematik, 1, 269-271, 1959.
  • T. H. Cormen, C. E. Leiserson, R. L. Rivest and C. Stein, Introduction to Algorithms, MIT Press, chapter on single-source shortest paths.
  • S. Russell and P. Norvig, Artificial Intelligence: A Modern Approach, 4th ed., Pearson, 2020, chapter 3 (breadth-first and uniform-cost search).

Knowledge check

Three conceptual questions. Write an answer, then reveal the explanation. Your answers stay in your browser.

Question 1

In the example graph, why is A not final when it is first pushed with cost 4?

Explanation

B has not been popped yet and can reach A for 1 + 2 = 3. Only when A is popped can no cheaper route exist, because every other unfinished node already costs at least as much as A.

Question 2

A corridor of 10 cells costs 1 per step. A detour through the waiting room is 6 cells but each cell costs 4. Which route does breadth-first search return, and which does Dijkstra?

Explanation

Breadth-first returns the 6-cell route (fewer steps) with cost 24. Dijkstra returns the 10-cell corridor with cost 10.

Question 3

What is the smallest change to the algorithm that makes Dijkstra return the nearest of three goals?

Explanation

Stop when the first goal is popped instead of when a particular goal is popped. Because nodes are popped in order of cost, the first goal popped is the cheapest.

What’s next

Next: A*, which adds an estimate of the distance still to go so the search leans towards the goal.

Practice this lesson

Implement Dijkstra

Scout needs the cheapest route to the pharmacy: implement Dijkstra on the hospital grid.

Next lesson

M3-T3 — A*: Search with a Heuristic

Trace A* with f = g + h, and explain why an admissible heuristic keeps it optimal while expanding fewer nodes.

Nothing below is required to finish this lesson.