Welcome to the final lesson of our "Foundations of Graph Theory" module. In our previous lessons, we've explored graphs from their basic definitions to performing traversals like BFS and DFS, and even used them to identify connected components. You've seen how BFS explores a graph in a systematic, level-by-level fashion. Today, we'll harness this specific property of BFS to solve a classic and highly practical problem: finding the shortest path between two nodes in an unweighted graph.
This ability to find the shortest path is not just a theoretical exercise; it's a fundamental concept that underlies many graph algorithms, including aspects of message passing in GNNs where information often flows along the shortest routes between nodes. Mastering this will complete your foundational toolkit before we move on to building neural networks with PyTorch.
The Shortest Path Problem
In an unweighted graph, the "length" or "cost" of any path is simply the number of edges it contains. The shortest path problem, therefore, is about finding a path between a source node and a destination node that consists of the minimum number of edges.
There are several variations of this problem, but today we will focus on the single-source shortest path problem, where we find the shortest path from a starting node to all other nodes. By solving this, we can easily find the specific path to any single destination.
Let's begin with a video that frames the problem and introduces the key idea.
Breadth First Search - Finding Shortest Paths in Unweighted Graphs
The video "Breadth First Search - Finding Shortest Paths in Unweighted Graphs" by Mary Elaine Califf clearly introduces the problem and the high-level intuition for the solution.
Watch from the beginning to the overview. Pay attention to the distinction between unweighted and weighted graphs and how the speaker describes BFS's layer-by-layer exploration.
Why BFS Guarantees the Shortest Path
The key insight is that BFS naturally discovers nodes in increasing order of their distance from the source. It explores all nodes at distance 1, then all nodes at distance 2, and so on. This creates an "expanding wavefront" moving out from the source node.

Because of this orderly, level-by-level exploration, the first time BFS encounters any node, it is guaranteed to have found a shortest path to it. If a shorter path existed, BFS would have had to traverse fewer edges, meaning it would have discovered the node at an earlier level, which is a contradiction.
For a great visual analogy that helps solidify this intuition, let's watch a brief clip.
Breadth First Search (BFS) Visualized and Explained
The video "Breadth First Search (BFS): Visualized and Explained" by Reducible provides a fantastic analogy for why BFS finds the shortest path.
Watch the segment from the string-pulling analogy. The idea of pulling up the start node and letting the strings tighten visually demonstrates how the shortest paths are revealed level by level.
The Algorithm: Modifying BFS for Path-Finding
A standard BFS traversal tells you if a path exists. To reconstruct the path itself, we need to make a small but crucial modification: we must keep track of how we arrived at each node.
We can achieve this by using a parent or predecessor data structure (like a dictionary or an array). When our BFS moves from a node u to its neighbor v, we record that we reached v from u by setting parent[v] = u.
The algorithm looks like this:
-
Initialization:
- Create a
queueand add thestartnode. - Create a
visitedset to avoid cycles and redundant work, adding thestartnode to it. - Create a
parentdictionary to store the path. Initialize it withparent[start] = None.
- Create a
-
Traversal:
- While the
queueis not empty:- Dequeue the current node,
u. - If
uis thegoalnode, stop the search. You've found the shortest path. - For each
neighborofu:- If the
neighborhas not been visited:- Add it to the
visitedset. - Set
parent[neighbor] = u. - Enqueue the
neighbor.
- Add it to the
- If the
- Dequeue the current node,
- While the
-
Path Reconstruction:
- If the
goalnode was found, trace your steps backward fromgoalusing theparentdictionary until you reach thestartnode. - This backward trace gives you the path in reverse, so you'll need to reverse it to get the final path from start to goal.
- If the
The following video segments walk through these data structures and the path reconstruction process in detail.
Breadth First Search - Finding Shortest Paths in Unweighted Graphs
These sections from Mary Elaine Califf's video detail the necessary data structures and the process for reconstructing the path once the destination is found.
First, watch the segment on data structures, focusing on the "previous vertex" array, which serves the same purpose as our parent dictionary. Then, skip ahead and watch the final section on path reconstruction. This shows exactly how to use the stored parent information to build the final path.
A Complete Example and Implementation
Let's tie everything together. The article "Shortest path in an unweighted graph" from GeeksforGeeks provides a concise summary and code structure. We'll then look at a clean Python implementation.
Shortest path in an unweighted graph
This article formalizes the approach we've discussed. We will focus on the logic, using the JavaScript code as a template since its syntax is close to Python.
First, read the Approach section to see a summary of the parent-tracking strategy. Next, examine the JavaScript code, focusing on two parts. First, the bfs function shows how the par (parent) and dist (distance) arrays are populated during the traversal. Look at the while loop within the bfs function. Then, look at the while loop inside the printShortestDistance function to see how the path is reconstructed by walking up the parent array.
Based on this logic, here is a complete Python implementation. This function takes a graph (represented as an adjacency list), a start node, and a goal node, and returns the shortest path as a list of nodes.
from collections import deque
def bfs_shortest_path(graph, start, goal):
# Keep track of explored nodes
explored = {start}
# Queue for BFS, storing paths
# The queue will store the path taken to reach a node
queue = deque([[start]])
if start == goal:
return [start]
while queue:
# Get the first path from the queue
path = queue.popleft()
node = path[-1]
# Go through all neighbors
for neighbour in graph.get(node, []):
if neighbour not in explored:
# Create a new path by extending the current one
new_path = list(path)
new_path.append(neighbour)
queue.append(new_path)
# Mark neighbor as explored
explored.add(neighbour)
# If we've reached the goal, return the path
if neighbour == goal:
return new_path
# If the queue becomes empty and goal was not found
return "No path found between the nodes."
# Example Usage:
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E', 'G'],
'G': ['F']
}
print(f"Shortest path from A to G: {bfs_shortest_path(graph, 'A', 'G')}")
# Expected output: ['A', 'C', 'F', 'G'] or ['A', 'B', 'E', 'F', 'G']
# The algorithm finds one of the shortest paths. In this case, there are two of length 3.
# Depending on neighbor order, it might find ['A', 'C', 'F', 'G']
Note: This implementation stores the entire path in the queue, which is intuitive but can use more memory than the parent-pointer method. For your GNN work, understanding the concept is key, and both implementation strategies are valid ways to think about it.
Conclusion
Congratulations on completing the first module! You have built a solid foundation in graph theory, moving from basic definitions to implementing sophisticated traversals for practical tasks.
Here are the key takeaways from today's lesson:
- BFS is the algorithm of choice for finding the shortest path in an unweighted graph.
- This works because BFS explores the graph level by level, guaranteeing that the first time it reaches a destination node, it has done so via a path with the minimum number of edges.
- To reconstruct the path, the standard BFS algorithm is modified to store the parent of each node as it's discovered.
- The final path is built by backtracking from the destination to the source using the stored parent pointers.
You are now well-prepared to tackle the next phase of our journey. In the upcoming module, "PyTorch Essentials for GNNs," we will switch gears and focus on the deep learning framework that will power our graph neural networks. We'll start with the basics of PyTorch tensors and build up to defining and training a simple neural network, equipping you with the skills needed to eventually merge graph theory and deep learning.