Table of Contents
- Introduction to Pathfinding and Weighted Graphs
- What is Dijkstra’s Algorithm?
- How Dijkstra’s Algorithm Works: Step-by-Step Explanation
- 3.1 Key Components
- 3.2 Step-by-Step Example
- Mathematical Formulation and Pseudocode
- Implementation in Python
- Time and Space Complexity Analysis
- Real-World Applications
- Limitations and Edge Cases
- Variations and Optimizations
- Conclusion
- References
1. Introduction to Pathfinding and Weighted Graphs
Before we explore Dijkstra’s Algorithm, let’s clarify the problem it solves.
What is a Weighted Graph?
A weighted graph is a set of nodes connected by edges, where each edge has a numerical “weight” (e.g., distance, cost, or time). Weights can be directed (one-way) or undirected (two-way). For example:
- A road network: Nodes = cities, edges = roads, weights = miles between cities.
- A social network: Nodes = users, edges = friendships, weights = interaction frequency.
The Shortest Path Problem
The shortest path problem asks: Given a weighted graph and a starting node (source), what is the minimum total weight (e.g., shortest distance, lowest cost) to reach every other node?
For unweighted graphs (all edges have equal weight = 1), BFS (Breadth-First Search) efficiently finds the shortest path by exploring nodes level by level. However, BFS fails for weighted graphs because it doesn’t account for edge weights—taking a longer edge first might lead to a suboptimal path.
Why Dijkstra’s?
Dijkstra’s Algorithm addresses this gap by greedily selecting the next node with the smallest known distance, updating paths to its neighbors, and repeating until all nodes are processed. It works for graphs with non-negative edge weights, making it ideal for most real-world scenarios (e.g., roads, networks, and financial transactions, where weights like distance or cost can’t be negative).
2. What is Dijkstra’s Algorithm?
Dijkstra’s Algorithm is a single-source shortest path algorithm developed by Dutch computer scientist Edsger W. Dijkstra in 1956. Its core goal is to compute the shortest path from a starting node (source) to all other nodes in a weighted graph with non-negative edge weights.
Key Insight: Greedy Selection
At each step, the algorithm selects the node with the smallest tentative distance (the shortest known path to that node so far), updates the distances to its neighbors, and marks it as “visited.” This greedy choice ensures that once a node is processed, we’ve found its shortest possible path.
3. How Dijkstra’s Algorithm Works: Step-by-Step Explanation
To understand Dijkstra’s Algorithm, let’s break down its components and walk through a concrete example.
3.1 Key Components
Dijkstra’s Algorithm relies on three main tools:
-
Distance Array: An array
dist[]wheredist[node]stores the shortest known distance from the source tonode. Initially, all distances are set to infinity (∞), except the source (distance = 0). -
Priority Queue (Min-Heap): A data structure that efficiently retrieves the node with the smallest tentative distance. This allows the algorithm to “greedily” select the next node to process.
-
Visited Tracking: A way to mark nodes as “processed” once their shortest path is confirmed (to avoid reprocessing).
3.2 Step-by-Step Example
Let’s apply Dijkstra’s Algorithm to a small weighted graph. We’ll use the graph below, where nodes represent cities and edges represent roads with mile weights:
Example Graph
- Nodes: A, B, C, D, E
- Edges (undirected, weights in miles):
- A ↔ B: 4 miles
- A ↔ C: 1 mile
- C ↔ B: 2 miles
- C ↔ D: 5 miles
- B ↔ D: 1 mile
- D ↔ E: 3 miles
Goal: Find the shortest path from source node A to all other nodes.
Step 1: Initialize Distances and Priority Queue
- Set
dist[]to[∞, ∞, ∞, ∞, ∞](indexes A, B, C, D, E). - Set
dist[A] = 0(distance from A to itself is 0). - Priority Queue starts with
(distance=0, node=A).
Step 2: Extract the Node with Minimum Distance
- Dequeue the node with the smallest distance:
(0, A). - Mark A as visited.
Step 3: Update Distances to Neighbors of A
A has neighbors B (4 miles) and C (1 mile). For each neighbor:
- Neighbor B: Current
dist[B] = ∞. New distance =dist[A] + weight(A→B) = 0 + 4 = 4. Since 4 < ∞, updatedist[B] = 4. Enqueue(4, B). - Neighbor C: Current
dist[C] = ∞. New distance =0 + 1 = 1. Updatedist[C] = 1. Enqueue(1, C).
Now:
dist[] = [0, 4, 1, ∞, ∞]- Priority Queue:
[(1, C), (4, B)]
Step 4: Extract Next Minimum Node (C)
Dequeue (1, C) (smallest distance). Mark C as visited.
Step 5: Update Distances to Neighbors of C
C has neighbors A (visited), B (2 miles), and D (5 miles).
- Neighbor B: Current
dist[B] = 4. New distance =dist[C] + weight(C→B) = 1 + 2 = 3. Since 3 < 4, updatedist[B] = 3. Enqueue(3, B). - Neighbor D: Current
dist[D] = ∞. New distance =1 + 5 = 6. Updatedist[D] = 6. Enqueue(6, D).
Now:
dist[] = [0, 3, 1, 6, ∞]- Priority Queue:
[(3, B), (4, B), (6, D)](note: There are two entries for B; we’ll handle this later).
Step 6: Extract Next Minimum Node (B)
Dequeue (3, B) (smallest distance). Mark B as visited.
Step 7: Update Distances to Neighbors of B
B has neighbors A (visited), C (visited), and D (1 mile).
- Neighbor D: Current
dist[D] = 6. New distance =dist[B] + weight(B→D) = 3 + 1 = 4. Since 4 < 6, updatedist[D] = 4. Enqueue(4, D).
Now:
dist[] = [0, 3, 1, 4, ∞]- Priority Queue:
[(4, B), (4, D), (6, D)]
Step 8: Extract Next Minimum Node (D)
Dequeue (4, D). Mark D as visited.
Step 9: Update Distances to Neighbors of D
D has neighbors C (visited), B (visited), and E (3 miles).
- Neighbor E: Current
dist[E] = ∞. New distance =dist[D] + weight(D→E) = 4 + 3 = 7. Updatedist[E] = 7. Enqueue(7, E).
Now:
dist[] = [0, 3, 1, 4, 7]- Priority Queue:
[(4, B), (6, D), (7, E)]
Step 10: Extract Remaining Nodes
- Dequeue
(4, B): B is already visited, so skip. - Dequeue
(6, D): D is already visited, so skip. - Dequeue
(7, E): Mark E as visited. No unvisited neighbors left.
Final Results
The shortest distances from A are:
- A: 0 miles
- B: 3 miles (A→C→B)
- C: 1 mile (A→C)
- D: 4 miles (A→C→B→D)
- E: 7 miles (A→C→B→D→E)
4. Mathematical Formulation and Pseudocode
Mathematical Formulation
Let ( G = (V, E) ) be a weighted graph with node set ( V ) and edge set ( E ). Let ( s \in V ) be the source node. For each node ( u \in V ), ( dist[u] ) is the shortest distance from ( s ) to ( u ).
- Initialize ( dist[s] = 0 ) and ( dist[u] = \infty ) for all ( u \neq s ).
- For each edge ( (u, v) \in E ) with weight ( w(u, v) ), if ( dist[v] > dist[u] + w(u, v) ), update ( dist[v] = dist[u] + w(u, v) ).
Pseudocode
Here’s a formal pseudocode for Dijkstra’s Algorithm using a priority queue:
function Dijkstra(Graph, source):
// Initialize distances
dist[source] ← 0
for each node u ≠ source:
dist[u] ← ∞
// Priority queue: (distance, node)
priority_queue ← [(0, source)]
visited ← empty set
while priority_queue is not empty:
// Extract node with smallest distance
current_dist, u ← extract-min(priority_queue)
if u in visited:
continue
add u to visited
// Update neighbors
for each neighbor v of u:
if dist[v] > dist[u] + weight(u, v):
dist[v] ← dist[u] + weight(u, v)
add (dist[v], v) to priority_queue
return dist
5. Implementation in Python
Let’s implement Dijkstra’s Algorithm in Python using an adjacency list to represent the graph and heapq for the priority queue.
Step 1: Represent the Graph
We’ll use an adjacency list (dictionary) where graph[node] returns a list of (neighbor, weight) tuples. For our example graph:
graph = {
'A': [('B', 4), ('C', 1)],
'B': [('A', 4), ('C', 2), ('D', 1)],
'C': [('A', 1), ('B', 2), ('D', 5)],
'D': [('B', 1), ('C', 5), ('E', 3)],
'E': [('D', 3)]
}
Step 2: Implement the Algorithm
import heapq
def dijkstra(graph, source):
# Initialize distances with infinity
dist = {node: float('infinity') for node in graph}
dist[source] = 0 # Distance to source is 0
# Priority queue: (distance, node)
priority_queue = [(0, source)]
visited = set()
while priority_queue:
# Extract node with smallest distance
current_dist, u = heapq.heappop(priority_queue)
# Skip if already visited
if u in visited:
continue
visited.add(u)
# Update neighbors
for v, weight in graph[u]:
if dist[v] > dist[u] + weight:
dist[v] = dist[u] + weight
heapq.heappush(priority_queue, (dist[v], v))
return dist
# Test the algorithm
shortest_distances = dijkstra(graph, 'A')
print("Shortest distances from A:")
for node, distance in shortest_distances.items():
print(f"{node}: {distance} miles")
Output
Shortest distances from A:
A: 0 miles
B: 3 miles
C: 1 miles
D: 4 miles
E: 7 miles
6. Time and Space Complexity Analysis
Time Complexity
The time complexity of Dijkstra’s Algorithm depends on the priority queue implementation:
-
Binary Heap (Most Common):
- Each edge ( (u, v) ) is processed once, leading to ( O(E) ) heap insertions.
- Each heap insertion/extraction takes ( O(\log V) ) time (since the heap size is at most ( V )).
- Total: ( O((V + E) \log V) ).
-
Array (Linear Search for Minimum):
- For dense graphs (( E \approx V^2 )), a linear search for the minimum node takes ( O(V) ) per iteration, leading to ( O(V^2) ) time.
- Better than binary heap for dense graphs (( O(V^2) ) vs. ( O(V^2 \log V) )).
-
Fibonacci Heap (Theoretical Optimum):
- Insertions and decrease-key operations take ( O(1) ) amortized time.
- Total: ( O(E + V \log V) ). However, Fibonacci heaps are complex to implement and rarely used in practice.
Space Complexity
- ( O(V) ) for the
distarray,visitedset, and priority queue (which stores at most ( V ) nodes).
7. Real-World Applications
Dijkstra’s Algorithm is ubiquitous in systems requiring efficient pathfinding. Here are key applications:
1. GPS Navigation
Apps like Google Maps and Waze use Dijkstra’s (or its variants) to compute the shortest/fastest route between two locations, considering real-time traffic (dynamic weights).
2. Network Routing Protocols
Protocols like OSPF (Open Shortest Path First) and BGP (Border Gateway Protocol) use Dijkstra’s to route data packets across the internet, minimizing latency or cost.
3. Airline Ticketing Systems
Airlines use Dijkstra’s to find the cheapest flight itineraries, where nodes are airports and edges are flights with fares as weights.
4. Robotics and Autonomous Vehicles
Robots and self-driving cars use Dijkstra’s to plan collision-free paths in environments modeled as weighted graphs (e.g., obstacles increase edge weights).
5. Game Development
In video games, Dijkstra’s (or A*, a heuristic variant) guides NPCs (non-player characters) to move efficiently through game worlds.
8. Limitations and Edge Cases
While powerful, Dijkstra’s Algorithm has critical limitations:
1. Fails with Negative Edge Weights
Dijkstra’s greedy approach assumes that once a node is processed, its shortest path is final. Negative edges can invalidate this: a later path through a negative edge might yield a shorter distance.
Example: Consider a graph with edges A→B (5), A→C (2), C→B (-4). Dijkstra’s would process A→C (distance 2), then B via C (2-4 = -2). But if there’s another edge B→C (1), creating a negative cycle (C→B→C: -4 + 1 = -3), the distance to B could be made infinitely small, which Dijkstra’s cannot detect.
2. No Support for Negative Cycles
A negative cycle (a cycle with total weight < 0) makes the shortest path undefined (you can loop indefinitely to reduce distance). Dijkstra’s ignores negative cycles and returns incorrect results.
3. Unreachable Nodes
Nodes unreachable from the source remain with distance ∞. The algorithm handles this gracefully but requires post-processing to identify unreachable nodes.
Workarounds
- For negative weights (no negative cycles): Use the Bellman-Ford Algorithm (slower, ( O(VE) )).
- For negative cycles: Bellman-Ford can detect them, but the shortest path is undefined.
9. Variations and Optimizations
1. A* Algorithm
A* (pronounced “A-star”) is an extension of Dijkstra’s that uses a heuristic function ( h(v) ) to guide the search toward the target node. This makes it faster for single-target pathfinding (e.g., games, GPS). The priority queue uses ( f(v) = g(v) + h(v) ), where ( g(v) ) is the distance from the source to ( v ), and ( h(v) ) is an estimate of the distance from ( v ) to the target.
2. Bidirectional Dijkstra
Instead of searching from the source alone, bidirectional Dijkstra searches from both the source and target, stopping when the searches meet. This reduces the search space, making it faster for large graphs (e.g., social networks with billions of nodes).
3. Contraction Hierarchies
A preprocessing technique that “contracts” nodes (removes less important nodes) to speed up query time. Used in GPS systems for near-instant route calculations.
4. Using a Fibonacci Heap
Though rarely implemented, Fibonacci heaps reduce the time complexity to ( O(E + V \log V) ), making them ideal for large sparse graphs.
10. Conclusion
Dijkstra’s Algorithm is a cornerstone of pathfinding in weighted graphs, beloved for its simplicity and efficiency with non-negative weights. By greedily selecting the shortest known path at each step, it efficiently computes distances from a source to all other nodes, powering applications from GPS to network routing.
While it struggles with negative weights and cycles, its variants (A*, Bellman-Ford) and optimizations (bidirectional search) extend its utility. Whether you’re building a game, optimizing a supply chain, or navigating a new city, understanding Dijkstra’s Algorithm unlocks the ability to solve complex pathfinding problems with confidence.
11. References
- Dijkstra, E. W. (1959). “A note on two problems in connexion with graphs.” Numerische Mathematik, 1(1), 269–271.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- Wikipedia. “Dijkstra’s algorithm.” Link
- GeeksforGeeks. “Dijkstra’s shortest path algorithm.” Link
- Khan Academy. “Dijkstra’s algorithm.” Link