Skip to content
Ameen Abozaid
PublishedBEG-043-L06

Demo: metro route finder with BFS

  • Lesson 6 of 6
  • 12 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

This is a hands-on lesson. You know the pieces: the adjacency list, the queue, the visited set, and BFS's fewest-edge guarantee. Here we assemble them into one working program that answers a real question: what is the route with the fewest stops between two stations? We build it step by step, the way you will build it in the module lab.

We also make one mistake on purpose: we leave out the visited set, run BFS on a map with a loop, and watch it fail to finish. Seeing the failure once makes the fix stick.

Everything below was compiled and run with JDK 21. Use any editor or IDE. Menus differ between tools, so the steps use plain files and the javac and java commands in a terminal, run from the folder that holds the file. If your IDE compiles for you, the code is identical. The file paths in the loader are placeholders, so adjust them to your own setup.

This lesson builds on all earlier lessons of this module, on Queue and ArrayDeque from the collections module, on String.split and Files.readAllLines from the earlier lessons on strings and files, and on Collections.reverse.

Learning Objectives

By the end of this lesson you will be able to:

  • Build a metro map as an adjacency list from lines such as Airport-Center.
  • Write findRoute(start, goal) with a queue and a visited structure.
  • Reproduce the missing-visited-set failure and fix it.
  • Store, for each station, the station we came from, and rebuild the route by walking back from the goal.
  • Return an empty list for an unknown or unreachable station instead of crashing.
  • Print the route and the number of stops.
  • Name which parts of the demo you will reuse in the lab.

Written Explanation

Step 1: the map

Here is our metro, as one track per line in the form StationA-StationB:

text
Airport-Center
Center-Market
Market-Harbor
Harbor-Park
Park-Center
Stadium-Center
Stadium-University
University-Library
Island-Dock

Two things are worth noticing before we code. Center is the busy hub: it connects to Airport, Market, Park, and Stadium. Center, Market, Harbor, and Park form a loop. And Island and Dock are connected to each other but not to the rest of the map, so a rider cannot go from Airport to Dock.

We reuse edge from earlier lessons to add both directions of each track:

java
static Map<String, List<String>> g = new HashMap<>();

static void edge(String a, String b) {
    g.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
    g.computeIfAbsent(b, k -> new ArrayList<>()).add(a);
}

To load the lines, split each at the dash and call edge:

java
for (String line : lines) {
    String[] p = line.split("-");
    edge(p[0], p[1]);
}

(Station names with a dash inside them would break this format. Our names have none.)

Step 2: a first BFS that only visits

Start with plain BFS from one station, printing the order, exactly as in Lesson 3. On this map it visits Center's neighbors first and then works outward. The visited set is what keeps the loop Center, Market, Harbor, Park from running forever.

Step 3: the planned mistake

To see why the visited set matters, here is BFS without it, on the smallest possible loop: A and B linked to each other. I cap the loop at 8 steps so the demo ends instead of hanging your terminal.

java
Map<String, List<String>> g = new HashMap<>();
g.put("A", List.of("B")); g.put("B", List.of("A"));
Queue<String> q = new ArrayDeque<>();
q.add("A");
int steps = 0;
while (!q.isEmpty() && steps < 8) {
    String cur = q.poll();
    System.out.println("visit " + cur);
    for (String n : g.get(cur)) q.add(n);
    steps++;
}
System.out.println("stopped by step limit, queue size " + q.size());

Output:

text
visit A
visit B
visit A
visit B
visit A
visit B
visit A
visit B
stopped by step limit, queue size 1

A and B keep adding each other, so the queue is never empty. Without the steps < 8 guard the loop would never end. Real programs show this as a process stuck at full speed on one CPU core, and in some cases with growing memory use. The fix is the visited set, marking nodes when they are added to the queue.

Step 4: remember where we came from

BFS finds the nodes, but we want the route. The trick is to remember, for each station we discover, which station discovered it. This is the "came from" map: Map<String, String> from a station to its predecessor.

The map doubles as the visited structure: a station is visited exactly when it is a key. The start station has no predecessor, so we store null for it. (A HashMap accepts null values, so this is legal.) When BFS reaches the goal, we start at the goal and follow cameFrom back until we reach the start, collecting stations, then reverse the list.

java
static List<String> findRoute(String start, String goal) {
    if (!g.containsKey(start) || !g.containsKey(goal)) return List.of();
    Map<String, String> cameFrom = new HashMap<>();
    Queue<String> q = new ArrayDeque<>();
    q.add(start);
    cameFrom.put(start, null);
    while (!q.isEmpty()) {
        String cur = q.poll();
        if (cur.equals(goal)) break;
        for (String next : g.get(cur)) {
            if (!cameFrom.containsKey(next)) {
                cameFrom.put(next, cur);
                q.add(next);
            }
        }
    }
    if (!cameFrom.containsKey(goal)) return List.of();
    List<String> route = new ArrayList<>();
    for (String s = goal; s != null; s = cameFrom.get(s)) route.add(s);
    Collections.reverse(route);
    return route;
}

Read it in four parts:

  1. Guard. If either station is not on the map, return an empty list right away.
  2. Search. The BFS loop. Stopping early with break when the goal is taken from the queue is safe, because BFS has already recorded the shortest way to every station closer than or equal to the goal.
  3. Not found. If the goal never became a key of cameFrom, it was unreachable, so return an empty list.
  4. Rebuild. Walk from the goal to the start with cameFrom.get(s). The walk ends when s becomes null, which only the start's entry contains. Reverse to get start-to-goal order.

An empty list is the signal for "no route". The caller decides what to print. We chose an empty list over an exception because an unreachable station is a normal answer to a normal question, not a program error.

Step 5: print the answer

java
static void show(String a, String b) {
    List<String> r = findRoute(a, b);
    if (r.isEmpty()) System.out.println(a + " -> " + b + ": no route");
    else System.out.println(a + " -> " + b + ": " + String.join(" > ", r)
        + " (" + (r.size() - 1) + " stops)");
}

A route with n stations has n - 1 moves. When we say "stops" here, we count the moves, that is edges, as in the earlier lessons. The start-equals-goal case gives a one-station route and 0 stops.

Step 6: loading from a file

For the lab, the map comes from a text file. With Files.readAllLines it is a few lines (the path is up to you, and a relative path depends on the folder you run the program from):

java
static void load(Path file) throws IOException {
    for (String line : Files.readAllLines(file)) {
        if (line.isBlank()) continue;
        String[] p = line.trim().split("-");
        edge(p[0], p[1]);
    }
}

I tested this on a temporary file containing A-B, B-C, a blank line, and C-D; it built {A=[B], B=[A, C], C=[B, D], D=[C]} and skipped the blank line. It requires import java.nio.file.*; and import java.io.IOException;. A malformed line with no dash would throw an ArrayIndexOutOfBoundsException at p[1]. A production loader would check p.length first, and that check is a good addition for the lab.

Examples

Example 1: a short route (simple)

java
show("Airport", "Harbor");

Output:

text
Airport -> Harbor: Airport > Center > Market > Harbor (3 stops)

Check on the map: Airport-Center, Center-Market, Market-Harbor are three tracks. The alternative Airport, Center, Park, Harbor is also three tracks. Both are shortest; BFS reports the one it met first, which depends on the order of Center's neighbor list. If a rider cares which, that needs another rule, not just BFS.

Example 2: a longer route and the trivial case (practical)

java
show("Library", "Market");
show("Park", "Park");

Output:

text
Library -> Market: Library > University > Stadium > Center > Market (4 stops)
Park -> Park: Park (0 stops)

Library has to go through University and Stadium to reach Center, then on to Market: four tracks. The second line shows that start equal to goal is handled without special code: the start is dequeued first, matches the goal, and the rebuild loop collects a single station.

Example 3: unreachable and unknown stations (realistic)

java
show("Airport", "Dock");
show("Airport", "Nowhere");

Output:

text
Airport -> Dock: no route
Airport -> Nowhere: no route

Dock exists but sits in the separate Island piece, so BFS from Airport never discovers it and the "not found" check returns an empty list. "Nowhere" is not on the map at all, so the guard returns immediately. Both cases print the same message here; if a user needs to know the difference, the guard could print "unknown station" itself. The program did not crash for either, which is the lab's acceptance rule.

Backend Insight

This small program has the shape of real routing and lookup code. A request comes in with two identifiers, the service validates them against the data it holds, searches, and returns either a result or a clear "none". Returning an empty result for "no route" and reserving errors for broken input is a convention you will use in APIs, where "not found" has its own response status that you will meet later in the roadmap.

Two design points carry over. First, the "came from" map is a general pattern: any search that must report a path, not just reach a goal, records predecessors and walks back. Second, validation belongs at the entry point. An unchecked unknown name flows into the algorithm and produces a confusing result, such as the one-element list for an unknown start that we saw in Lesson 3. Check inputs first, search second.

Common Mistakes

Mistake 1: Leaving out the visited structure

It makes BFS loop forever on any map with a loop, and every undirected edge is a loop of two. In findRoute, the cameFrom map is the visited structure, so never add to the queue without putting the station into it first.

Mistake 2: Marking visited when taking from the queue

Mark when you add. Otherwise one station can enter the queue several times, and if the code overwrites cameFrom each time, a later discoverer can replace the first one. Keep the first.

Mistake 3: Forgetting to reverse the route

Walking back from the goal gives goal-to-start order. Without Collections.reverse, the printed route runs backward.

Mistake 4: Crashing on unknown or unreachable stations

g.get("Nowhere") is null, and looping over it throws a NullPointerException. Check containsKey first, and test the unreachable case separately from the unknown case.

Mistake 5: Using DFS for the route

DFS would find some route, not the fewest-stops route. The lab requires BFS for the route and allows DFS only for the "can I get there at all" check.

Exercises

Try each exercise yourself before opening the solutions. Use the metro map from this lesson.

Level A: Foundation

A1. Give the fewest-stops route from Library to Center, and its stops.

Show solution A1

A1. Library, University, Stadium, Center: 3 stops.

A2. What does findRoute("Airport", "Dock") return, and why?

Show solution A2

A2. An empty list. Dock is in the Island piece, which is not connected to Airport, so BFS never reaches it and cameFrom never gets a Dock key.

A3. In findRoute, what does the cameFrom map store for the start station, and what ends the rebuild loop?

Show solution A3

A3. It stores null, because the start has no predecessor. The rebuild loop for (String s = goal; s != null; ...) ends when s becomes null, which happens right after the start has been added.

Level B: Application

B1. Give a fewest-stops route from Stadium to Harbor and its stops. Is it unique?

Show solution B1

B1. For example Stadium, Center, Market, Harbor: 3 stops. It is not unique: Stadium, Center, Park, Harbor also has 3 stops. BFS returns one of them.

B2. Why is it safe to stop BFS as soon as the goal is taken from the queue?

Show solution B2

B2. BFS takes stations from the queue in order of distance, and a station's cameFrom entry is set when it is first discovered, from an earlier station on a shortest path. By the time the goal is taken from the queue, its entry is already final, so no later work can improve it.

B3. Explain what would go wrong if findRoute marked a station as visited only when it was taken from the queue.

Show solution B3

B3. The same station could be added to the queue by several neighbors before it is processed, so the queue holds duplicates. If the code also overwrote cameFrom each time, a later discoverer could replace the first. Marking on add avoids both problems.

Level C: Challenge

C1. Write static int stops(String a, String b) that returns the number of stops, or -1 if there is no route. Reuse findRoute.

Show solution C1

C1.

java
static int stops(String a, String b) {
    List<String> r = findRoute(a, b);
    return r.isEmpty() ? -1 : r.size() - 1;
}

stops("Park","Park") gives 0, and stops("Airport","Dock") gives -1. I compiled the lesson program with JDK 21, but I did not run this exact helper; it only combines findRoute with a size check.

C2. The city adds a new track Library-Airport. What is the new fewest-stops route from Library to Market, and how many stops does it have? Does the old route still count as a valid route?

Show solution C2

C2. The new route is Library, Airport, Center, Market: 3 stops, one fewer than the old 4. The old route is still a valid route, just not a shortest one. BFS finds the 3-stop route automatically; no code change is needed. I traced this by hand and did not run it.

Mini Challenge

Turn the demo into the first half of the lab. Create a text file of tracks, station-station per line, with at least eight stations, two lines crossing at one station, and one separate piece. Then write a program that:

  1. Loads the file into an adjacency list. Choose a path that works on your computer, since folder layouts and IDE run folders differ.
  2. Reads two station names (from args or hard-coded) and prints the route and the number of stops.
  3. Handles an unknown station and an unreachable station with different messages.
  4. Prints the BFS route for at least five pairs, including start equal to goal.

Then, for the lab's last item, add a short DFS reachable(from, to) and write one sentence on when you would use it instead of BFS (you only need a yes or no, not the route).

Check your work: your hand-counted stops match the output, and no pair of stations crashes the program.

Summary & Next Lesson

Key takeaways

  • The route finder is BFS plus a cameFrom map: discover stations, remember who discovered each, and walk back from the goal.
  • Without a visited structure BFS never finishes on a map with a loop. In findRoute the cameFrom map plays that role.
  • Reverse the walked-back list to get the route in start-to-goal order.
  • Validate station names first. Return an empty list for unknown or unreachable stations rather than crashing.
  • Several routes can tie for the fewest stops. BFS reports one of them.

Next lesson

This was the last lesson of the module. The module lab, "Metro route finder", asks you to load a small metro map from a text file, implement BFS for the fewest-stops route, handle unknown and unreachable stations, and add a DFS "can I get there at all" check.

Self-check

  1. What does the cameFrom map store for each station?
  2. How do you rebuild the route after BFS reaches the goal?
  3. What does findRoute return when no route exists?
  4. Why must visited be marked when a station is added to the queue?
  5. When would you use DFS instead of BFS on the metro map?
Show answers
  1. The station from which it was first discovered (and null for the start).
  2. Follow cameFrom from the goal back to the start, then reverse the list.
  3. An empty list.
  4. So a station can never be queued twice, which would loop forever on a cycle and waste work.
  5. When you only need a yes or no to "can I get there at all", not the fewest-stops route.

References

  • CP-Algorithms, Breadth First Search. Supports that BFS yields fewest-edge paths in unweighted graphs and that a path is restored by following parent links from the target back to the source and reversing; an unvisited target means no path exists.
  • Oracle, ArrayDeque (Java SE 21 API). Supports poll returning null on an empty deque and the absence of a capacity limit.
  • Oracle, Map (Java SE 21 API). Supports computeIfAbsent. That HashMap permits null values is standard behavior that I confirmed by running the program, not from a fetched page.
  • The route outputs, the infinite-loop demonstration (capped at 8 steps), and the file loader were compiled and run with JDK 21. The metro map is an invented teaching example. The statement about what a stuck process looks like (CPU use, memory) is general experience that I did not measure.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Type to search levels, phases, modules and lessons.