Find the Shortest Path in a Graph Using Dijkstra’s Algorithm

problem-solving
Published

December 23, 2024

Finding the shortest path between two points is a fundamental problem in computer science with applications ranging from GPS navigation to network routing. Dijkstra’s algorithm provides an efficient solution for finding the shortest paths from a single source node to all other nodes in a graph with non-negative edge weights. This post will walk you through the implementation of Dijkstra’s algorithm in Python, explaining the concepts along the way.

Understanding Dijkstra’s Algorithm

Dijkstra’s algorithm works by iteratively exploring nodes in a graph, maintaining a set of nodes whose shortest distance from the source has already been determined. It starts at the source node and assigns it a distance of 0. Then, it repeatedly selects the node with the smallest tentative distance and explores its neighbors, updating their tentative distances if a shorter path is found. This process continues until all reachable nodes have been visited.

The algorithm relies on these key data structures:

  • graph: A dictionary representing the graph where keys are nodes and values are dictionaries of neighbors with associated edge weights. For example: {'A': {'B': 4, 'C': 2}, 'B': {'A': 4, 'D': 5}, 'C': {'A': 2, 'E': 3}, 'D': {'B': 5, 'F': 2}, 'E': {'C':3, 'F':4}, 'F': {'D':2, 'E':4}}

  • distances: A dictionary storing the shortest distance from the source node to each node. Initialized with infinity for all nodes except the source, which is 0.

  • visited: A set keeping track of visited nodes.

  • predecessors: A dictionary to track the path.

Python Implementation

Here’s a Python implementation of Dijkstra’s algorithm:

import sys

def dijkstra(graph, source):
    distances = {node: sys.maxsize for node in graph}
    distances[source] = 0
    visited = set()
    predecessors = {}

    while len(visited) < len(graph):
        min_distance = sys.maxsize
        current_node = None

        for node, distance in distances.items():
            if node not in visited and distance < min_distance:
                min_distance = distance
                current_node = node

        if current_node is None:
            break

        visited.add(current_node)

        for neighbor, weight in graph[current_node].items():
            new_distance = distances[current_node] + weight
            if new_distance < distances[neighbor]:
                distances[neighbor] = new_distance
                predecessors[neighbor] = current_node

    return distances, predecessors


def shortest_path(predecessors, target):
    path = []
    current_node = target
    while current_node is not None:
        path.insert(0, current_node)
        current_node = predecessors.get(current_node)
    return path


# Example graph
graph = {'A': {'B': 4, 'C': 2}, 'B': {'A': 4, 'D': 5}, 'C': {'A': 2, 'E': 3}, 'D': {'B': 5, 'F': 2}, 'E': {'C':3, 'F':4}, 'F': {'D':2, 'E':4}}

distances, predecessors = dijkstra(graph, 'A')

print("Shortest distances from A:", distances)

target_node = 'F'
shortest_path_to_target = shortest_path(predecessors, target_node)
print(f"Shortest path from A to {target_node}: {shortest_path_to_target}")

This code first implements the dijkstra function which calculates the shortest distances from the source node. Then, the shortest_path function reconstructs the actual path using the predecessors dictionary. The example demonstrates how to use the functions with a sample graph.

Handling Different Graph Representations

The above example uses a dictionary representation of the graph. You can change the code to work with other representations like adjacency matrices or edge lists, but you’ll need to modify how you access neighbor nodes and edge weights accordingly. The core logic of Dijkstra’s algorithm remains the same.

Improving Efficiency

For very large graphs, you might consider using more advanced data structures like Fibonacci heaps to improve the efficiency of finding the node with the minimum distance in each iteration. However, for many practical applications, the provided implementation is sufficiently efficient.