BFS: the fewest stops
In short: BFS (breadth-first search) explores a graph layer by layer with a queue and finds the path with the fewest edges in O(V + E).
Problem
Six streets instead of four
The courier dispatcher wants a route from the depot to the hospital with the fewest streets. In this city every street means a traffic light, a turn and a stop, so the number of streets matters more than the metres.
A colleague wrote a search that takes the first free street and keeps going until it arrives. If it gets stuck, it goes back and tries the next one:
function anyRoute(graph, at, target, seen) { if (at === target) return [at]; seen.add(at); for (const next of graph.get(at)) { if (seen.has(next)) continue; const rest = anyRoute(graph, next, target, seen); if (rest !== null) return [at, ...rest]; } return null;}const route = anyRoute(graph, "Depot", "Hospital", new Set());console.log(`${route.join(" → ")}: ${route.length - 1} streets`);Depot → Station → Square → Market → Park → Stadium → Hospital: 6 streetsThere is a route, but it has 6 streets. Look at the map from the previous lesson: Depot → Station → Square → Cinema → Hospital is 4. The search did not make a mistake: it never asked for the shortest one, it took the first one it found.
The idea
A wave from the depot
Imagine a wave leaving the depot. First it reaches all stops one street away. Then from them all new stops two streets away. Then three. A stop the wave has reached is taken: the next waves do not touch it.
So a stop gets its distance at the moment the wave touches it for the first time, and that is the smallest one possible.
The order "nearer ones first" is kept by a queue: found stops join at the back, and the one being handled leaves from the front. This is BFS (breadth-first search).
distkeeps the distance of every found stop. It is also the set of seen stops: a stop with adistdoes not enter the queue a second time.prevkeeps where every stop was found from. With it the route itself is rebuilt later.
See it
Layer by layer
function bfs(graph, start) { const dist = new Map([[start, 0]]); const prev = new Map(); const queue = [start]; for (let head = 0; head < queue.length; head++) { const stop = queue[head]; for (const next of graph.get(stop)) { if (dist.has(next)) continue; dist.set(next, dist.get(stop) + 1); prev.set(next, stop); queue.push(next); } } return { dist, prev };}function bfs(graph, start) { const dist = new Map([[start, 0]]); const prev = new Map(); const queue = [start]; for (let head = 0; head < queue.length; head++) { const stop = queue[head]; for (const next of graph.get(stop)) { if (dist.has(next)) continue; dist.set(next, dist.get(stop) + 1); prev.set(next, stop); queue.push(next); } } return { dist, prev };}
function pathTo(prev, target) { const path = []; let at = target; while (at !== undefined) { path.push(at); at = prev.get(at); } return path.reverse();}Step by step, in words
- Start at Depot: dist = 0, and it is the only stop in the queue.
- Depot leaves the queue. Its neighbours Station and Market are new: dist = 1, prev = Depot, and they join the back of the queue.
- Station: Depot already has a dist and is skipped. Square gets dist = 2.
- Market also sees Square, but Square is already in the queue with dist = 2. Every stop enters the queue only once. Park: dist = 2.
- Layer 2 leaves the queue and opens layer 3: Cinema across the bridge and Stadium. The queue always holds at most two layers, the nearer one in front.
- Cinema finds Hospital: dist = 4. When Stadium also reaches it, Hospital already has a dist: the first arrival is the shortest one.
- Hospital has nothing new around it. The queue is empty: every stop has its distance from Depot.
- pathTo walks back along prev: Hospital ← Cinema ← Square ← Station ← Depot. Reversed, that is the route: 4 streets.
The program runs BFS from the depot, prints the distance to every stop and the route to the hospital:
const { dist, prev } = bfs(graph, "Depot");for (const stop of stops) { console.log(`${stop}: ${dist.get(stop)}`);}const path = pathTo(prev, "Hospital");console.log(`${path.join(" → ")}: ${path.length - 1} streets`);Depot: 0Station: 1Market: 1Square: 2Park: 2Cinema: 3Hospital: 4Stadium: 3Depot → Station → Square → Cinema → Hospital: 4 streetsThe array is a queue with an index head: push puts at the back, and head shows which one is in front. Nothing is removed from the start of the array (why, see "More precisely" below). How the queue works inside is in the lesson "Stack and queue".
Try it
Put the lines in order
This is the heart of BFS: one turn of the loop. Put the lines in order:
const stop = queue[head];}if (dist.has(next)) continue;prev.set(next, stop);dist.set(next, dist.get(stop) + 1);for (const next of graph.get(stop)) {queue.push(next);
Try it
Predict the output
The same BFS, but it starts from Cinema. What does the program print for Depot, Stadium and Park?
const { dist } = bfs(graph, "Cinema");const ask = ["Depot", "Stadium", "Park"];console.log(ask.map((s) => dist.get(s)).join(" "));The idea
The route backwards
dist says how many streets there are, but not which ones. The route is in prev: Hospital was found from Cinema, Cinema from Square, Square from Station, Station from Depot. Depot has no prev, because everything started there.
That is why the route is read from the back: from the target along prev to the start, and reversed at the end.
function pathTo(prev, target) { const path = []; let at = target; while (at !== undefined) { path.push(at); at = prev.get(at); } return path.reverse();}let at = target;
while (at !== undefined) {
path.push(at);
at = ;
}The next stop back is the one from which the current one was found: prev.get(at). At the start there is no prev, the value is undefined, and the loop stops.
More precisely, if you are curious
The routes in prev form a tree: every stop points to exactly one other, the one it was found from, and the root is the depot. This is called the BFS tree. Every branch from the root to a stop is the shortest route to it. That is why one BFS from the depot gives the shortest routes to all stops at once, not only to the hospital.
If a stop was never found (there is no route), it has no dist. Check that before you call pathTo: otherwise you get a "route" of one stop, the target itself.
The idea
Why the first arrival is the shortest
The queue holds at most two layers: d and d + 1, with the nearer one in front. A stop d + 1 streets away is found from a stop d streets away, and stops d + 2 away come only when the ones at d + 1 start to leave. So a stop gets its dist from the shortest route, and every later route to it is at least as long and is skipped.
It also matters when a stop is marked as seen: at the moment it enters the queue, not when it leaves. Otherwise Square, which is seen both from Station and from Market, would enter twice.
More precisely, if you are curious
The proof in one sentence: by induction on d, all stops at distance d leave the queue before all stops at distance d + 1, and each gets dist = d. This holds only if every edge costs 1. If the streets have different lengths, fewer streets does not mean fewer metres. Then the queue is replaced by a priority queue by the distance travelled, and that is Dijkstra (the Dijkstra lesson in this module).
The idea
What it costs
Every stop enters the queue at most once and leaves once. When it leaves, it looks at its streets. A two-way street is seen from both of its ends. In total: V departures and 2E looks, O(V + E).
That holds for an adjacency list. With a matrix "who are the neighbours" is the whole row, and BFS makes V looks per stop: O(V²). Run both on a grid city of n crossroads:
- BFS with an adjacency list
O(n) - BFS with a matrix
O(n²)
| Algorithm | n = 10 | n = 1,000 | n = 5,000 | n = 100,000 |
|---|---|---|---|---|
| BFS with an adjacency list | 36 | 4,872 | 24,716 | 498,734 |
| BFS with a matrix | 110 | 1,001,000 | 25,005,000 | ≈ 10,002,000,000 |
function bfs(graph, start) { const dist = new Map([[start, 0]]); const prev = new Map(); const queue = [start]; for (let head = 0; head < queue.length; head++) { const stop = queue[head]; for (const next of graph.get(stop)) { if (dist.has(next)) continue; dist.set(next, dist.get(stop) + 1); prev.set(next, stop); queue.push(next); } } return { dist, prev };}Every stop that leaves the queue and every look at a neighbour is counted. At 1000 crossroads the list makes 4,872 operations, and the matrix 1,001,000. At 100,000 crossroads the list makes 498,734, about 5 per crossroad: one departure and up to 4 streets. The matrix at 5000 is already at 25,005,000.
Try it
Find the bug
A colleague decided that the distance of a new stop is "the next number". The program prints:
function bfs(graph, start) { const dist = new Map([[start, 0]]); const prev = new Map(); const queue = [start]; for (let head = 0; head < queue.length; head++) { const stop = queue[head]; for (const next of graph.get(stop)) { if (dist.has(next)) continue; dist.set(next, dist.size); prev.set(next, stop); queue.push(next); } } return { dist, prev };}Depot: 0Station: 1Market: 2Square: 3Park: 4Cinema: 5Hospital: 7Stadium: 6Depot → Station → Square → Cinema → Hospital: 4 streetsThe route is right, 4 streets, and yet Hospital comes out at 7. Which line has the bug?
Pick the line with the bug.
The distance of a new stop is the distance of the one you found it from, plus one street: dist.get(stop) + 1. The number of found stops is only its position in the queue. The two match only as long as every layer has one stop.
More precisely
The queue in every language
JavaScript has no built-in queue. queue.shift() removes the first element and by the specification moves all the others: O(n) per take. The array with an index head removes nothing, it only moves the index: O(1). The price is that the array keeps all past stops until BFS ends, but there are V of them, as many as in dist.
Exercise
The layers of the city
Solve the task in your editor.
Layers and bridges. With BFS from the depot print the stops by layers (the stops of each layer in the order of stops) and the farthest stop. Then close both bridges: remove them from the adjacency list, run BFS again and print which stops can no longer be reached.
const bridges = [ ["Market", "Park"], ["Square", "Cinema"],];Layer 0: DepotLayer 1: Station, MarketLayer 2: Square, ParkLayer 3: Cinema, StadiumLayer 4: HospitalFarthest: Hospital (4 streets)Without the bridges you cannot reach: Park, Cinema, Hospital, StadiumMore tasks
- The nearest pharmacy. Some stops have a pharmacy. Run one BFS that starts from all pharmacies at once (all of them are in the queue with
dist0), and print for every stop how many streets away the nearest one is. - A maze of shelves. The warehouse is a grid of shelves (
#) and aisles (.). Find the fewest steps from the entrance to the exit, where the neighbours of a cell are up, down, left and right. - All the shortest. Count how many different shortest routes there are from the depot to every stop.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Big-O: how the work grows · Space complexity · Stack and queue · Graph: adjacency list and matrix
- Continues in: DFS: islands and cycles · Topological sorting · Dijkstra · City navigation
- Tasks on this topic: Graphs