Dijkstra's Algorithm: Finding Shortest Paths in Weighted Graphs
Master Dijkstra's algorithm for single-source shortest path problems in weighted graphs with positive edges, including implementations and trade-offs.
Dijkstra’s Algorithm: Finding Shortest Paths in Weighted Graphs
When you need to find the shortest path from one source vertex to all other vertices in a weighted graph, Dijkstra’s algorithm is the gold standard. It works on graphs with non-negative edge weights and delivers optimal paths with efficient time complexity. From GPS navigation to network routing, Dijkstra powers countless real-world systems that depend on finding optimal routes quickly.
The algorithm greedily selects the unvisited vertex with the smallest known distance, then explores its neighbors, updating distances if a shorter path is found. This simple but powerful strategy yields correctness because any path to a vertex must go through some intermediate vertex—and if we’re processing vertices in order of increasing distance, we’ll always discover the optimal path first.
Introduction
Dijkstra’s algorithm solves the single-source shortest path problem: given a weighted graph and a source vertex, find the minimum distance from the source to every other vertex. It achieves this with a greedy strategy—repeatedly select the unvisited vertex with the smallest known distance, then update distances to its neighbors. This deceptively simple approach delivers optimal paths in O((V + E) log V) time with a priority queue, making it one of the most important algorithms in computer science.
The elegance of Dijkstra lies in its correctness proof: any path to a vertex must go through some intermediate vertex, and if we process vertices in order of increasing distance from the source, we’ll always discover the optimal path to a vertex before we finalize its distance. This property holds only for non-negative edge weights—if negative edges existed, a shorter path could be found after a vertex was processed, invalidating the greedy choice. This constraint shapes where Dijkstra can be applied, but positive weights are common in real-world problems like road networks, routing tables, and game graph traversal.
Dijkstra appears everywhere in practice: GPS navigation systems find optimal routes, network protocols compute shortest paths for packet routing, game AI calculates movement costs, and dependency resolvers determine installation order. Understanding Dijkstra means understanding priority queue implementation trade-offs (binary heap vs Fibonacci heap), how to reconstruct shortest paths, and recognizing when its assumptions (non-negative weights) are violated. Variations like A* with heuristics build on Dijkstra’s foundation, making it essential knowledge for any graph algorithm work.
How Dijkstra’s Algorithm Works
The algorithm maintains a priority queue (min-heap) of vertices keyed by their current known distance from the source. Starting with the source at distance 0 and all others at infinity, we repeatedly extract the vertex with minimum distance and relax its outgoing edges.
import heapq
from collections import defaultdict
def dijkstra(graph, source):
"""
Single-source shortest paths with Dijkstra's algorithm.
Args:
graph: Adjacency list dict[node -> [(neighbor, weight), ...]]
source: Starting vertex
Returns:
dict mapping each vertex to its shortest distance from source
"""
dist = {source: 0}
pq = [(0, source)] # (distance, vertex) min-heap
visited = set()
while pq:
current_dist, u = heapq.heappop(pq)
if u in visited:
continue
visited.add(u)
# Skip if we've found a better path already
if current_dist > dist.get(u, float('inf')):
continue
for v, weight in graph[u]:
if v not in visited:
new_dist = current_dist + weight
if new_dist < dist.get(v, float('inf')):
dist[v] = new_dist
heapq.heappush(pq, (new_dist, v))
return dist
Path Reconstruction
Knowing distances is useful, but often you need the actual path. Track predecessors during relaxation:
def dijkstra_with_path(graph, source, target):
"""Returns (distance, path_list) from source to target."""
dist = {source: 0}
pred = {source: None}
pq = [(0, source)]
visited = set()
while pq:
current_dist, u = heapq.heappop(pq)
if u == target:
# Reconstruct path
path = []
while u is not None:
path.append(u)
u = pred[u]
return current_dist, path[::-1]
if u in visited:
continue
visited.add(u)
for v, weight in graph[u]:
if v not in visited:
new_dist = current_dist + weight
if new_dist < dist.get(v, float('inf')):
dist[v] = new_dist
pred[v] = u
heapq.heappush(pq, (new_dist, v))
return float('inf'), [] # Target unreachable
Dijkstra’s Algorithm Flow
graph TD
A["Start: dist[source] = 0<br/>dist[others] = ∞"] --> B["Initialize priority queue<br/>with source at distance 0"]
B --> C["Extract minimum distance<br/>unvisited vertex u"]
C --> D{"Is u the target?"}
D -->|Yes| E["Return distance<br/>and reconstruct path"]
D -->|No| F[Mark u as visited]
F --> G{"For each neighbor v of u not visited"}
G -->|"new_dist < dist[v]"| H["Update dist[v] = new_dist<br/>pred[v] = u<br/>Push v to queue"]
H --> G
G -->|"new_dist >= dist[v]"| G
G --> I{"Queue empty?"}
I -->|Yes| J["Return all distances<br/>unreachable = ∞"]
I -->|No| C
E --> K[End]
When to Use Dijkstra’s Algorithm
Use Dijkstra when:
- You need single-source shortest paths
- All edge weights are non-negative
- You need efficient extraction of minimum distance vertices
- The graph is sparse (works better than dense matrix representations)
Don’t use Dijkstra when:
- Graph has negative edge weights (use Bellman-Ford instead)
- You need all-pairs shortest paths (consider Floyd-Warshall for small dense graphs)
- You need to detect negative cycles
Trade-off Analysis
| Aspect | Dijkstra | Bellman-Ford | Floyd-Warshall |
|---|---|---|---|
| Time Complexity | O((V + E) log V) | O(V × E) | O(V³) |
| Space Complexity | O(V + E) | O(V) | O(V²) |
| Negative Weights | No | Yes | Yes |
| Single Source | Yes | Yes | Yes (all pairs) |
| Negative Cycle Detection | No | Yes | Yes |
Production Failure Scenarios
-
Negative edge weights silently producing wrong results: Dijkstra’s assumes non-negative weights. If negative edges slip in, the algorithm may return incorrect shortest paths without any error indication. The greedy extraction guarantee breaks because a vertex extracted at distance X could later receive a shorter path through an unvisited negative edge. Always validate graph input to reject or handle negative weights before running Dijkstra.
-
Priority queue saturation under high edge counts: In dense graphs with E ≈ V², the min-heap can contain many entries per vertex as each relaxation pushes a new candidate. This causes heap operations to dominate runtime and memory. Monitor heap size growth and switch to the adjacency matrix variant when graph density exceeds ~50%.
-
Integer overflow with large distances: When edge weights are large or paths long, cumulative distance can overflow floating-point representation. Use a large constant (like
10**18) instead offloat('inf')and check for overflow on each relaxation. -
Graph with unreachable vertices: Always initialize distances properly and handle the case where target is unreachable. An infinite distance in the result means the vertex is not reachable from source.
Implementation Variants
# Using adjacency matrix (O(V²) - better for dense graphs)
def dijkstra_dense(graph, source):
n = len(graph)
dist = [float('inf')] * n
dist[source] = 0
visited = [False] * n
for _ in range(n):
# Find minimum unvisited vertex
u = -1
min_dist = float('inf')
for i in range(n):
if not visited[i] and dist[i] < min_dist:
min_dist = dist[i]
u = i
if u == -1:
break
visited[u] = True
for v in range(n):
if not visited[v] and graph[u][v] != float('inf'):
new_dist = dist[u] + graph[u][v]
if new_dist < dist[v]:
dist[v] = new_dist
return dist
Quick Recap Checklist
- Use min-heap (priority queue) for efficient minimum extraction
- Initialize source distance to 0, all others to infinity
- Only process each vertex once (mark as visited)
- Relax edges from current vertex when distance is confirmed
- Reconstruct paths by tracking predecessors
- Don’t use with negative edge weights
Observability Checklist
Track Dijkstra’s algorithm runs to catch performance degradation and incorrect results.
Core Metrics
- Vertex extraction count (should be exactly V for complete runs)
- Edge relaxation count per vertex
- Priority queue size at each extraction
- Distance convergence per vertex
- Final distance values for all reachable vertices
Health Signals
- Extraction count exceeding V (potential loop or visited-set bug)
- Priority queue growing unbounded (heap saturation indicator)
- Distances not decreasing monotonically
- Same vertex extracted multiple times with different distances
- Runtime exceeding expected O((V + E) log V)
Alerting Thresholds
- Extraction count > V × 1.5: visited set implementation bug
- Priority queue size > 3V: graph density problem or relaxation bug
- Single vertex extracted > log V times: priority queue issue
- Runtime p99 > 100ms for V < 10K: investigate heap performance
Distributed Tracing
- Trace Dijkstra runs with vertex count, edge count, and heap operations
- Include source and target vertices in span attributes
- Correlate slow runs with high-density graphs or large V
- Track relaxation count as proxy for graph connectivity
Security Notes
Dijkstra’s algorithm has security implications when graph input is untrusted.
Graph poisoning with negative weights
If an attacker can control graph topology and injects negative-weight edges, Dijkstra’s will silently produce incorrect shortest paths. The algorithm assumes non-negative weights and the greedy extraction guarantee breaks with negative edges. No error is raised—the results are simply wrong.
Fix: Validate all edge weights before running Dijkstra. Reject graphs containing any negative weights, or use Bellman-Ford which handles negative weights correctly. Log when negative weights are encountered.
Priority queue exhaustion via crafted dense graphs
An attacker who controls graph input can craft a dense graph causing heap operations to dominate runtime. With E ≈ V², each of V vertex extractions must scan E edges, causing O(V³) behavior instead of O((V + E) log V).
Fix: Set maximum limits on vertex and edge counts. Detect graph density at input time and switch to the O(V²) adjacency matrix variant for dense graphs. Set time budgets per extraction and abort if exceeded.
Path traversal attacks via specially crafted shortest paths
If Dijkstra output drives downstream path traversal (routing, navigation), an attacker who crafts the graph topology can direct traversal through specific nodes for reconnaissance or exploitation.
Fix: Validate that computed paths conform to expected topology constraints. Do not blindly trust shortest-path output without verifying the path makes sense in your application context.
Interview Questions
Dijkstra's greedy approach relies on the assumption that once we extract a vertex with minimum distance, that distance is final. With negative edges, a shorter path could still be discovered later through an unvisited vertex. For example, if we process vertex A at distance 5, then later discover a negative edge from unvisited B to A with weight -10, A's distance should become -5—but we've already marked it as visited.
With a binary min-heap, it's O((V + E) log V)—extracting minimum is O(log V) done V times, and each edge relaxation may insert into the heap (E times). With an adjacency matrix, it's O(V²) which is better for dense graphs where E ≈ V². The Fibonacci heap variant achieves O(V log V + E) but with high constant factors.
Dijkstra's will find one optimal path, but if you need all paths with the same minimum distance, track a list of predecessors for each vertex instead of a single one. When relaxing an edge that would create an equal-distance path, add the new predecessor to the list rather than replacing.
No, Dijkstra's cannot find longest paths. The longest path problem is NP-hard. Dijkstra's greedy guarantee only holds when we're always choosing the minimum—if we needed maximum, the greedy choice wouldn't lead to an optimal solution because a longer path now could enable even longer paths later.
The core data structures are:
- Priority queue (min-heap) — For extracting the vertex with the smallest known distance in O(log V) time. A binary heap is the most common choice.
- Distance array/hash map — Tracks the current shortest distance from source to each vertex.
- Visited set/array — Prevents reprocessing vertices whose shortest distance has been finalized.
- Predecessor array/hash map — Stores the previous vertex on the shortest path for path reconstruction.
- Adjacency list — Efficiently iterates over neighbors during edge relaxation.
A grid can be treated as a graph where each cell is a vertex connected to its adjacent cells (up, down, left, right):
- Convert the 2D grid into an adjacency list — each cell connects to traversable neighbors with unit weight (or weighted by movement cost).
- Obstacle cells are excluded from the graph entirely (or marked as untraversable).
- Use a coordinate pair (row, col) as the vertex identifier in the priority queue.
- A* search is often preferred over Dijkstra's for grid pathfinding because the Manhattan or Euclidean heuristic prunes unnecessary exploration.
Key differences:
- Edge weights — BFS works on unweighted graphs or graphs with equal-weight edges; Dijkstra handles arbitrary non-negative weights.
- Data structure — BFS uses a simple queue (FIFO); Dijkstra uses a priority queue (min-heap).
- Time complexity — BFS is O(V + E); Dijkstra is O((V + E) log V) with a binary heap.
- Optimality — Both find shortest paths for their respective domains. BFS finds the fewest edges; Dijkstra finds the lowest cumulative weight.
- When both work — For unweighted graphs, BFS is faster and simpler than Dijkstra.
Unreachable vertices remain at their initial distance of infinity (or a sentinel large value). The algorithm's termination condition is the priority queue becoming empty, not all vertices being visited. After execution:
- Any vertex with distance = infinity is unreachable from the source.
- In path reconstruction, unreachable targets return an empty path or a special error indicator.
- Applications should check for infinite distances in the result before using the path.
A* is an extension of Dijkstra's algorithm with a heuristic function h(n) that estimates the cost from vertex n to the target:
- Ordering key — Dijkstra uses the known distance g(n) alone; A* uses f(n) = g(n) + h(n), combining known distance with estimated remaining distance.
- Efficiency — A* explores fewer vertices when a good heuristic is available, making it faster for point-to-point pathfinding.
- Optimality — A* is optimal if the heuristic is admissible (never overestimates the true cost).
- Use case — Dijkstra is better for single-source to all-vertices; A* is better for single-source to single-target with spatial information.
- Convergence — A* with h(n) = 0 degenerates to Dijkstra's algorithm.
Input validation strategies:
- Edge weight check — Scan all edges for negative weights before execution. If found, either reject the input or fall back to Bellman-Ford.
- Graph density detection — If E ≈ V², consider switching to the O(V²) adjacency matrix variant to avoid priority queue overhead.
- Vertex/edge count limits — Enforce maximum bounds to prevent priority queue saturation or memory exhaustion.
- Source validity — Verify the source vertex exists in the graph.
- Isolated vertices — Detect vertices with no edges to avoid unnecessary processing.
Path reconstruction tracks the predecessor of each vertex during edge relaxation:
- Initialize a predecessor map with the source set to None.
- Whenever a shorter distance is found for vertex v via vertex u, set pred[v] = u.
- After the algorithm finishes, reconstruct the path from target to source by following predecessor pointers backwards.
- Reverse the resulting list to get the path from source to target.
- If the target has no predecessor (None), it is unreachable from the source.
Dijkstra's algorithm handles disconnected graphs correctly:
- Vertices in the same connected component as the source will receive correct shortest distances.
- Vertices in disconnected components (no path from source) will remain at distance infinity.
- The algorithm terminates when the priority queue is empty, regardless of whether all vertices have been visited.
- This behavior is correct by design — unreachable vertices should have infinite distance in the output.
Real-world optimizations include:
- Bidirectional Dijkstra — Run searches simultaneously from source and target, meeting in the middle. This can reduce the search space by roughly half.
- ALT (A*, Landmarks, Triangle inequality) — Precompute distances to a set of landmark vertices to derive admissible heuristics for A*-like pruning.
- Contraction Hierarchies (CH) — Preprocess the graph by contracting low-importance vertices, creating shortcut edges. Enables near-instantaneous queries.
- Edge partitioning — Partition the graph into hierarchical levels (highways vs. local roads) for multi-level routing.
- Cache-friendly graph layout — Store adjacency lists contiguously in memory to exploit CPU cache lines.
Space complexity analysis:
- Adjacency list representation — O(V + E) for storing the graph graph structure.
- Priority queue — O(V) in the worst case (one entry per vertex), but can grow to O(E) if multiple distance updates push duplicate entries.
- Distance array — O(V) storage.
- Visited set — O(V) storage.
- Predecessor map — O(V) for path reconstruction.
- Concern points — For graphs with millions of vertices, the priority queue can consume significant memory if duplicate entries accumulate. Use a decrease-key operation or lazy deletion to mitigate this.
Different priority queue implementations yield different time complexities:
- Binary heap — O((V + E) log V). Most common choice. Good balance of performance and simplicity.
- Fibonacci heap — O(V log V + E). Theoretically faster with O(1) amortized decrease-key, but high constant factors make it slower in practice for most graphs.
- d-ary heap — O((V + E) log_V / log_d). Can outperform binary heap for dense graphs by reducing the number of comparisons.
- Pairing heap — Competitive practical performance with simpler implementation than Fibonacci heap.
- Unsorted array — O(V²) for extracting minimum. Only suitable for dense graphs where E ≈ V².
Yes, Dijkstra's algorithm handles zero-weight edges correctly:
- Zero-weight edges are valid non-negative edges and do not break the correctness proof.
- Multiple vertices connected by zero-weight edges will all receive the same distance as the source (distance propagates without increasing).
- The visited-set check prevents infinite loops — once a vertex is visited, it is not reprocessed even if another zero-weight path could lead to it.
- BFS on unweighted graphs is essentially Dijkstra's with all edges having weight 1 or 0 if edges are uniform.
Verification strategies:
- Unit tests with known graphs — Test on small graphs where shortest paths can be manually verified.
- Compare with Bellman-Ford — Run both algorithms on the same non-negative graph and assert identical distance results.
- Triangle inequality property — Verify that dist[u] + weight(u,v) >= dist[v] for all edges after execution (shortest path property).
- Path consistency check — For each vertex, follow its predecessor chain and verify the reconstructed path's total distance matches the stored distance.
- Stress testing — Random graph generation with known properties (e.g., all edges weight 1, results should match BFS).
- Negative weight rejection — Confirm the implementation either rejects negative weights or produces a clear error.
Although both use a similar greedy structure with priority queues, they solve different problems:
- Objective — Dijkstra finds shortest paths from a source to all vertices; Prim finds a minimum spanning tree that connects all vertices with minimum total weight.
- Distance metric — Dijkstra accumulates distance from source; Prim tracks the cheapest edge weight connecting a vertex to the growing tree.
- Priority key — Dijkstra's key is the total path distance from source; Prim's key is the minimum edge weight connecting the vertex to the tree so far.
- Output — Dijkstra outputs distance from source to each vertex; Prim outputs the set of edges forming the MST.
- Visited set — Both mark vertices as visited, but Dijkstra finalizes distances whereas Prim finalizes the cheapest connection to the tree.
On a DAG, Dijkstra's algorithm works correctly but may not be the optimal choice:
- Dijkstra's correctness holds on DAGs with non-negative weights since the greedy extraction guarantee does not depend on graph cycles.
- However, a simpler O(V + E) approach exists for DAGs: topological sort followed by a single pass of edge relaxations. This avoids the priority queue overhead entirely.
- For DAGs with negative weights, the topological approach still works (since there are no cycles), whereas Dijkstra would fail with negative edges.
- Dijkstra's advantage over the topological approach is that it can stop early when targeting a specific vertex.
Yen's algorithm is a classic approach for finding k shortest loopless paths:
- First, run Dijkstra's to find the shortest path and add it to a result list.
- For each vertex on the shortest path, create a deviation: temporarily remove the edge to the next vertex and find the shortest path from the current vertex to the target.
- Use a candidate min-heap ordered by total path weight to track the next best alternatives.
- Pop the smallest candidate path, add it to results, and generate new deviations from it.
- Repeat until k paths are found or no more candidates exist.
- Each iteration requires O(V + E log V) for the deviation run, making the total O(k · (V + E log V)).
Further Reading
Dive deeper into Dijkstra’s algorithm and related topics with these resources:
Books
- “Introduction to Algorithms” (CLRS) — Chapter 24 covers single-source shortest paths with rigorous proofs of Dijkstra’s correctness.
- “Algorithms” by Robert Sedgewick & Kevin Wayne — Practical implementations and applications of Dijkstra’s algorithm in graph theory.
Academic Papers
- Edsger W. Dijkstra (1959) — “A Note on Two Problems in Connexion with Graphs” — The original paper describing the algorithm.
- Michael L. Fredman & Robert E. Tarjan (1987) — “Fibonacci Heaps and Their Uses in Improved Network Optimization Algorithms” — Introduces the Fibonacci heap variant achieving O(V log V + E).
Online Resources
- Visualgo.net - Dijkstra’s Algorithm Visualization — Interactive step-by-step visualization.
- USACO Guide - Dijkstra’s Algorithm — Competitive programming focused treatment.
- CP-Algorithms - Dijkstra’s Algorithm — Detailed explanation with multiple language implementations.
Related Algorithms
- Bellman-Ford Algorithm — Handles negative edge weights and detects negative cycles at O(V × E) cost.
- Floyd-Warshall Algorithm — All-pairs shortest paths in O(V³) for dense graphs.
- A* Search — Extension of Dijkstra’s with heuristics for goal-directed pathfinding.
- Bidirectional Dijkstra — Runs two simultaneous searches from source and target for faster convergence.
Conclusion
Dijkstra’s algorithm finds single-source shortest paths by greedily selecting the unvisited vertex with minimum distance and relaxing its edges. It runs in O((V + E) log V) with a binary min-heap and requires non-negative edge weights—use Bellman-Ford or Floyd-Warshall when negative weights are present. For dense graphs, the adjacency matrix variant runs in O(V²). Path reconstruction requires tracking predecessor pointers during relaxation.
Category
Related Posts
Bellman-Ford Algorithm: Shortest Paths with Negative Weights
Learn the Bellman-Ford algorithm for single-source shortest paths including negative edge weights and negative cycle detection.
Floyd-Warshall Algorithm: All-Pairs Shortest Paths
Master the Floyd-Warshall algorithm for finding shortest paths between all pairs of vertices in weighted graphs, with implementation examples and trade-offs.
AVL Trees: Self-Balancing Binary Search Trees
Master AVL tree rotations, balance factors, and rebalancing logic. Learn when to use AVL vs Red-Black trees for your use case.