Dijkstra’s Algorithm - Theory
"The art of programming is the art of organizing complexity." - Edsger W. Dijkstra
Problem Statement & Mathematical Foundation
Formal Definition
Given a weighted directed graph G = (V, E) with:
- V: Set of vertices (nodes)
- E: Set of edges with non-negative weights
- w(u,v) ≥ 0 for all edges (u,v) ∈ E
- Source vertex s ∈ V
Goal: Find shortest path distance from s to all vertices v ∈ V
Key Properties:
- Triangle Inequality: d(u,w) ≤ d(u,v) + d(v,w)
- Optimal Substructure: Subpaths of shortest paths are shortest paths
- Greedy Choice: Selecting closest unvisited vertex leads to optimal solution
Why Non-Negative Weights?
s --(-5)--> v ---(1)---> t
s -------(2)-----------> t
Greedy would pick s→t (cost 2) but optimal is s→v→t (cost -4)
Historical Context & Motivation
Conceived by Edsger Dijkstra in 1956 at Mathematical Centre in Amsterdam:
- Solved shortest route problem by hand
- First published in 1959
- Evolved with priority queue optimizations (1970s)
- Fibonacci heap improvements (1980s)
- Modern GPU parallelization
Deep Intuition & Mental Models
1. Ripple Effect
- Like water ripples spreading uniformly
- Each "wavefront" represents distance threshold
- Reaches vertices in order of increasing distance
2. Greedy Explorer
- Always visits nearest unvisited location first
- Updates neighbor distances progressively
- Never revisits thanks to non-negative weights
3. Spring Relaxation
- Edges are springs with natural lengths (weights)
- Initially all distances "tense" (infinite)
- Relaxation reduces tension by finding shorter paths
Why Greedy Works: Next closest vertex must have optimal distance - any shorter path would contradict its selection.
🔍 Algorithm Analysis
Core Structure
function dijkstra(graph, source):
dist = array of size V, initialized to ∞
dist[source] = 0
priority_queue = min-heap containing all vertices
while priority_queue not empty:
u = extract_min(priority_queue)
for each neighbor v of u:
if dist[u] + weight(u,v) < dist[v]:
dist[v] = dist[u] + weight(u,v)
decrease_key(priority_queue, v, dist[v])
Loop Invariants
dist[u] ≤ actual_shortest_distance(source, u)for all u- For processed vertices,
dist[u] = actual_shortest_distance - Priority queue contains unprocessed vertices with best estimates
Termination Proof: Processes each vertex exactly once (V iterations).
Data Structure Deep Dive
Priority Queue Showdown
| Operation | Binary Heap | Fibonacci Heap | Array |
|---|---|---|---|
| Extract-min | O(log V) | O(log V) amor. | O(V) |
| Decrease-key | O(log V) | O(1) amor. | O(1) |
| Best for | Sparse graphs | Theoretical opt | Dense graphs |
| Total Complexity | O((V+E)log V) | O(E + V log V) | O(V²) |
Java PriorityQueue Tip:
// Handle duplicates with distance check
if (currentDist > dist[u]) continue;
Implementation Variants
1. Standard Implementation
// O((V+E)log V) using binary heap
class DijkstraStandard {
public int[] dijkstra(List<List<Edge>> graph, int source) {
int V = graph.size();
int[] dist = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[source] = 0;
PriorityQueue<Pair> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a.dist));
pq.offer(new Pair(source, 0));
while (!pq.isEmpty()) {
Pair current = pq.poll();
int u = current.node;
if (current.dist > dist[u]) continue;
for (Edge edge : graph.get(u)) {
int newDist = dist[u] + edge.weight;
if (newDist < dist[edge.to]) {
dist[edge.to] = newDist;
pq.offer(new Pair(edge.to, newDist));
}
}
}
return dist;
}
}
2. Path Reconstruction
class DijkstraWithPath {
public List<Integer> shortestPath(List<List<Edge>> graph, int source, int target) {
int V = graph.size();
int[] dist = new int[V];
int[] parent = new int[V];
Arrays.fill(dist, Integer.MAX_VALUE);
Arrays.fill(parent, -1);
dist[source] = 0;
// ... same as standard implementation ...
// During relaxation:
if (newDist < dist[v]) {
dist[v] = newDist;
parent[v] = u; // Track parent
pq.offer(new Pair(v, newDist));
}
// Reconstruct path
List<Integer> path = new LinkedList<>();
for (int at = target; at != -1; at = parent[at]) {
path.add(0, at);
}
return path;
}
}
3. Bidirectional Search
class BidirectionalDijkstra {
public int shortestPath(List<List<Edge>> graph, List<List<Edge>> reverseGraph,
int source, int target) {
// Initialize forward and backward searches
int[] distForward = new int[V];
int[] distBackward = new int[V];
// ... both initialized to ∞ except sources
Set<Integer> visitedForward = new HashSet<>();
Set<Integer> visitedBackward = new HashSet<>();
int bestPath = Integer.MAX_VALUE;
while (!pqForward.isEmpty() && !pqBackward.isEmpty()) {
// Forward step
int u = pqForward.poll().node;
visitedForward.add(u);
if (visitedBackward.contains(u))
bestPath = Math.min(bestPath, distForward[u] + distBackward[u]);
// Backward step
int v = pqBackward.poll().node;
visitedBackward.add(v);
if (visitedForward.contains(v))
bestPath = Math.min(bestPath, distForward[v] + distBackward[v]);
// Relax neighbors in respective graphs
}
return bestPath;
}
}
Edge Cases & Pitfalls
-
Integer Overflow:
// Safe addition check if (dist[u] > Integer.MAX_VALUE - weight) throw new ArithmeticException("Overflow risk"); -
Disconnected Graphs: Unreachable nodes remain at ∞
-
Duplicate Edges: Always keep minimum weight:
graph.computeIfAbsent(u, k -> new HashMap<>()) .merge(v, weight, Math::min); -
Self-Loops: Skip during neighbor iteration
if (edge.to == u) continue; -
Negative Weights:
if (hasNegativeWeights(graph)) throw new IllegalArgumentException("Use Bellman-Ford instead");
Optimizations & Variants
A* Search (Heuristic-Guided)
class AStar {
public int shortestPath(List<List<Edge>> graph, int source, int target,
Heuristic heuristic) {
// f(n) = g(n) + h(n)
PriorityQueue<AStarNode> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a.fScore));
pq.offer(new AStarNode(source, 0, heuristic.estimate(source, target)));
while (!pq.isEmpty()) {
AStarNode current = pq.poll();
if (current.node == target) return current.gScore;
for (Edge edge : graph.get(current.node)) {
int newGScore = current.gScore + edge.weight;
int newFScore = newGScore + heuristic.estimate(edge.to, target);
pq.offer(new AStarNode(edge.to, newGScore, newFScore));
}
}
return -1; // Unreachable
}
}
Parallel Dijkstra
class ParallelDijkstra {
public int[] parallelDijkstra(List<List<Edge>> graph, int source) {
AtomicIntegerArray dist = new AtomicIntegerArray(V);
// Initialize with MAX_VALUE except source
PriorityBlockingQueue<Pair> pq = new PriorityBlockingQueue<>();
pq.offer(new Pair(source, 0));
ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
for (int i = 0; i < numThreads; i++) {
executor.submit(() -> {
while (!pq.isEmpty()) {
Pair current = pq.poll();
int u = current.node;
if (current.dist > dist.get(u)) continue;
for (Edge edge : graph.get(u)) {
int newDist = dist.get(u) + edge.weight;
// Atomic compare-and-set
while (newDist < dist.get(edge.to)) {
int currentDist = dist.get(edge.to);
if (dist.compareAndSet(edge.to, currentDist, newDist)) {
pq.offer(new Pair(edge.to, newDist));
break;
}
}
}
}
});
}
// Shutdown executor and return results
}
}
Real-World Applications
-
GPS Navigation:
enum OptimizationCriteria { SHORTEST_DISTANCE, FASTEST_TIME, AVOID_TOLLS } int calculateWeight(RoadSegment seg, OptimizationCriteria criteria) { switch (criteria) { case FASTEST_TIME: return seg.travelTime; case AVOID_TOLLS: return seg.hasTolls ? seg.travelTime * 10 : seg.travelTime; default: return seg.distance; } } -
Network Routing:
// OSPF-style cost calculation int cost = latency * (1.0 / reliability) * (REFERENCE_BANDWIDTH / bandwidth); -
Game AI Pathfinding:
// Terrain-based movement costs enum Terrain { GRASS(1), WATER(3), MOUNTAIN(5) } int weight = current.terrain.cost + neighbor.terrain.cost;
Interview Patterns
Pattern 1: Basic Implementation
public int networkDelayTime(int[][] times, int n, int k) {
// Build graph
List<List<int[]>> graph = new ArrayList<>();
for (int i = 0; i <= n; i++) graph.add(new ArrayList<>());
for (int[] t : times) graph.get(t[0]).add(new int[]{t[1], t[2]});
int[] dist = new int[n+1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[k] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[1]-b[1]);
pq.offer(new int[]{k, 0});
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int u = curr[0], d = curr[1];
if (d > dist[u]) continue;
for (int[] edge : graph.get(u)) {
int newDist = d + edge[1];
if (newDist < dist[edge[0]]) {
dist[edge[0]] = newDist;
pq.offer(new int[]{edge[0], newDist});
}
}
}
int max = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == Integer.MAX_VALUE) return -1;
max = Math.max(max, dist[i]);
}
return max;
}
Pattern 2: Constrained Shortest Path
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
// dist[node][stops] = cost
int[][] dist = new int[n][k+2];
for (int i = 0; i < n; i++) Arrays.fill(dist[i], Integer.MAX_VALUE);
dist[src][0] = 0;
// [node, stops, cost]
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[2]-b[2]);
pq.offer(new int[]{src, 0, 0});
while (!pq.isEmpty()) {
int[] curr = pq.poll();
int u = curr[0], stops = curr[1], cost = curr[2];
if (u == dst) return cost;
if (stops > k) continue;
for (int[] f : flights) {
if (f[0] == u) {
int newCost = cost + f[2];
if (newCost < dist[f[1]][stops+1]) {
dist[f[1]][stops+1] = newCost;
pq.offer(new int[]{f[1], stops+1, newCost});
}
}
}
}
return -1;
}
Pattern 3: Grid Pathfinding
public int minPathCost(int[][] grid, int[][] moveCost) {
int m = grid.length, n = grid[0].length;
int[][] dist = new int[m][n];
for (int i = 0; i < m; i++) Arrays.fill(dist[i], Integer.MAX_VALUE);
for (int j = 0; j < n; j++) dist[0][j] = grid[0][j];
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[2]-b[2]);
for (int j = 0; j < n; j++) pq.offer(new int[]{0, j, grid[0][j]});
while (!pq.isEmpty()) {
int[] cell = pq.poll();
int r = cell[0], c = cell[1], cost = cell[2];
if (r == m-1) continue;
int cellVal = grid[r][c];
for (int j = 0; j < n; j++) {
int newCost = cost + moveCost[cellVal][j] + grid[r+1][j];
if (newCost < dist[r+1][j]) {
dist[r+1][j] = newCost;
pq.offer(new int[]{r+1, j, newCost});
}
}
}
return Arrays.stream(dist[m-1]).min().getAsInt();
}
This comprehensive guide covers Dijkstra's algorithm from theoretical foundations to advanced optimizations and real-world applications. The key insights:
- Non-negative weights are essential for correctness
- Priority queue choice dramatically affects performance
- State space expansion handles constrained problems
- Bidirectional search optimizes single-pair queries
- Heuristics (A)* improve performance for targeted searches
Dijkstra remains fundamental to pathfinding in transportation, networking, and game AI 65+ years after its invention, proving the enduring value of elegant algorithm design.