Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 53

Shortest Path with BFS

Recommended resources

Hello! Welcome to your next lesson on graph algorithms.

In our previous lesson, we established the two fundamental ways to represent a graph in code: adjacency matrices and adjacency lists. We concluded that for the sparse graphs common in interviews, the adjacency list is usually the superior choice due to its space efficiency.

Today, we'll put that representation to work. We'll explore our first major graph traversal algorithm, Breadth-First Search (BFS). This algorithm is a cornerstone of your interview toolkit, not just for exploring a graph, but for its most famous application: finding the shortest path between two nodes in an unweighted graph. This directly addresses our learning outcome: Implement Breadth-First Search (BFS) to find the shortest path in unweighted graphs.

By the end of this lesson, you'll understand the mechanics of BFS, be able to implement it to find and reconstruct a shortest path, and recognize when to apply it to common interview problems.

1. The Core Idea of BFS: Exploring Level by Level

At its heart, Breadth-First Search is a systematic way to explore a graph. Imagine dropping a pebble into a pond; the ripples expand outwards one circle at a time. BFS does the same, starting from a source node and visiting all its immediate neighbors first (level 1), then all of their unvisited neighbors (level 2), and so on.

This level-by-level exploration is the key to why BFS is perfect for finding the shortest path in an unweighted graph. In an unweighted graph, the shortest path is simply the path with the fewest edges. Because BFS explores layer by layer, the very first time it reaches a destination node, it is guaranteed to have done so via a path with the minimum number of edges.

To build a strong intuition for this, watch the first part of the following video. It uses a great "balls with strings" analogy to visualize this level-by-level traversal.

Breadth First Search (BFS): Visualized and Explained

This video from Reducible provides a fantastic visual intuition for why BFS finds the shortest path by exploring the graph in layers.

Watch from the beginning to 02:38. Focus on how the algorithm explores nodes at distance 1, then distance 2, and so on, guaranteeing that the first time a node is visited, it's via the shortest path.


2. The BFS Algorithm: Data Structures and Steps

To implement this level-by-level strategy, BFS relies on a few key data structures. Since you have a solid background in DSA, these should be familiar, but their roles in BFS are specific and important.

Breadth-First Search (BFS) - Shortest Paths in Unweighted ...

The article from InterviewCake provides a concise breakdown of the data structures needed for a robust BFS implementation. It's crucial for understanding not just the traversal, but also the path reconstruction.

Read the section 'Understanding the Core Concepts of BFS'. Pay close attention to the roles of the Queue, the Visited Hash Set, and the Predecessor Hash Map.

Let's summarize the roles of these components:

  • Queue: This is the engine of BFS. It operates in a First-In, First-Out (FIFO) manner. We add a node's neighbors to the back of the queue and process nodes from the front. This ensures we finish exploring one "level" before starting the next.
  • Visited Set: This is essential for efficiency and correctness. By tracking the nodes we've already visited (or added to the queue), we avoid redundant processing and prevent getting stuck in infinite loops in graphs with cycles.
  • Predecessor (or Parent) Map: This is the key to our learning outcome. Simply traversing the graph isn't enough; we need to find the path. This map stores the "parent" of each node in the path (i.e., predecessor[neighbor] = current_node). Once we find our destination, we can backtrack through this map to reconstruct the shortest path.

With these data structures, the algorithm proceeds as follows. The video below provides a clear, animated walkthrough of these steps.

Breadth First Search Algorithm | Shortest Path | Graph Theory

This video by WilliamFiset demonstrates the BFS algorithm in action, showing how the queue and visited nodes are managed. It then presents clear pseudocode for both the search and the path reconstruction.

Please watch these three segments: 00:40 - 03:12: Watch the animation to see how the queue dictates the traversal order. 03:26 - 06:15: Follow the pseudocode for the main solve method. Note the use of the queue, visited array, and prev array. 06:15 - 07:16: Understand the logic for the reconstructPath method, which uses the prev array.


3. Implementing BFS for Shortest Path in Python

Now, let's translate this into Python code. Here is a function that finds the shortest path in an unweighted graph represented by an adjacency list. It returns the path as a list of nodes, or None if no path exists.

Notice how the code directly maps to the concepts we've discussed: queue, visited, and predecessor_map.

from collections import deque

def bfs_shortest_path(graph, start_node, end_node):
    """
    Finds the shortest path between start_node and end_node in an unweighted graph using BFS.

    Args:
        graph (dict): The graph as an adjacency list.
        start_node: The starting node.
        end_node: The target node.

    Returns:
        list: A list of nodes representing the shortest path, or None if no path exists.
    """
    if start_node not in graph or end_node not in graph:
        return None # Or raise an error

    # A deque is a highly efficient queue in Python
    queue = deque([start_node]) 
    
    # Keep track of visited nodes to avoid cycles
    visited = {start_node}
    
    # The predecessor map is key for reconstructing the path
    # format: {node: predecessor_node}
    predecessor_map = {start_node: None}

    while queue:
        current_node = queue.popleft()

        if current_node == end_node:
            # We found the target! Now, reconstruct the path.
            path = []
            node = end_node
            while node is not None:
                path.append(node)
                node = predecessor_map[node]
            return path[::-1] # Reverse the path to get start -> end

        for neighbor in graph.get(current_node, []):
            if neighbor not in visited:
                visited.add(neighbor)
                predecessor_map[neighbor] = current_node
                queue.append(neighbor)

    # If the queue becomes empty and we haven't found the end_node
    return None # No path exists

# --- Example Usage ---
# Let's use the graph from the GeeksforGeeks article
# V=8, E=10, S=2, D=6
edges = [
    [0, 1], [1, 2], [0, 3], [3, 4],
    [4, 7], [3, 7], [6, 7], [4, 5],
    [4, 6], [5, 6]
]
num_nodes = 8
adj_list = {i: [] for i in range(num_nodes)}
for u, v in edges:
    adj_list[u].append(v)
    adj_list[v].append(u)

start = 2
end = 6
path = bfs_shortest_path(adj_list, start, end)

print(f"Graph: {adj_list}")
print(f"Shortest path from {start} to {end}: {path}") 
# Expected Output: [2, 1, 0, 3, 4, 6]

Let's visualize the process for a simpler path, say from A to E.

BFS Shortest Path Algorithm Visualization
CAPTION: This image shows a step-by-step BFS traversal from node A to E. It clearly illustrates the state of the queue and the visited set at each step. In Step 3, node E is found. The path would be reconstructed by backtracking from E to its predecessor B, and from B to its predecessor A, giving the path A -> B -> E.
Test your understanding!

Using the predecessor_map from the Python code, imagine a BFS is run on a graph and a path is found. The final predecessor_map looks like this:

{'A': None, 'B': 'A', 'C': 'A', 'D': 'C', 'F': 'B', 'E': 'D'}

What is the shortest path from node 'A' to node 'E'? Trace the steps you would take to reconstruct it.

Show answer

The shortest path is ['A', 'C', 'D', 'E'].

Reconstruction Steps:

  1. Start at the end_node, which is 'E'. Add it to the path: path = ['E'].
  2. Look up its predecessor: predecessor_map['E'] is 'D'. Add 'D' to the path: path = ['E', 'D'].
  3. Look up 'D's predecessor: predecessor_map['D'] is 'C'. Add 'C': path = ['E', 'D', 'C'].
  4. Look up 'C's predecessor: predecessor_map['C'] is 'A'. Add 'A': path = ['E', 'D', 'C', 'A'].
  5. 'A's predecessor is None, so we stop.
  6. Finally, reverse the list to get the correct order: ['A', 'C', 'D', 'E'].

4. Complexity Analysis

In an interview, after implementing an algorithm, you will always be asked about its complexity.

  • Time Complexity:

    • Each vertex () is added to the queue and processed exactly once.
    • Every edge () is examined exactly once (or twice in an undirected graph, which is still proportional to ).
    • Therefore, the total time is proportional to the number of vertices plus the number of edges.
  • Space Complexity:

    • In the worst case (e.g., a star graph where the center node is the source), the queue can hold up to nodes.
    • The visited set and predecessor_map will also store up to entries.
    • Thus, the space required scales linearly with the number of vertices.

5. Application: BFS on a Grid

A very common interview pattern is to ask a problem that can be modeled as a graph traversal on a 2D grid (like a maze or a pixel matrix). In these problems:

  • Each cell is a vertex.
  • Adjacent cells (up, down, left, right) are its neighbors.
  • Obstacles (like walls) define which edges don't exist.

Finding the "shortest path" in a maze is a classic BFS problem. The "Flood Fill" algorithm, used in applications like MS Paint's bucket tool, is another great example.

Breadth First Search (BFS): Visualized and Explained

The Reducible video we saw earlier has an excellent second half that applies BFS to solve the 'Flood Fill' problem. This will help you see how to translate a grid-based problem into a graph traversal.

Watch from 05:21 to the end. Focus on how the grid cells are treated as nodes and how the 'neighbors' function is implemented to handle grid boundaries.


Conclusion

Great job! You've just mastered one of the most fundamental and useful algorithms for your technical interviews. BFS is your go-to tool whenever a problem asks for the shortest path in an unweighted graph or involves level-by-level exploration.

Key Takeaways:

  • BFS explores a graph level by level using a FIFO queue.
  • It is guaranteed to find the shortest path in terms of the number of edges (for unweighted graphs).
  • The implementation requires three key data structures: a queue for managing nodes to visit, a visited set to avoid redundant work, and a predecessor map to reconstruct the path.
  • The time complexity is and space complexity is .
  • BFS is frequently applied to grid-based problems by treating cells as nodes and adjacent cells as neighbors.

Preview of the Next Lesson:

Now that we've covered breadth, it's time to explore depth. In our next lesson, we will implement Depth-First Search (DFS). While BFS uses a queue to go wide, DFS uses a stack to go deep. We'll compare its implementation and complexity to BFS and discover the unique set of problems where DFS is the more powerful tool.