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:

Goal: Find shortest path distance from s to all vertices v ∈ V

Key Properties:

  1. Triangle Inequality: d(u,w) ≤ d(u,v) + d(v,w)
  2. Optimal Substructure: Subpaths of shortest paths are shortest paths
  3. 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:

Deep Intuition & Mental Models

1. Ripple Effect

2. Greedy Explorer

3. Spring Relaxation

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

  1. dist[u] ≤ actual_shortest_distance(source, u) for all u
  2. For processed vertices, dist[u] = actual_shortest_distance
  3. 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;
    }
}
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

  1. Integer Overflow:

    // Safe addition check
    if (dist[u] > Integer.MAX_VALUE - weight) 
        throw new ArithmeticException("Overflow risk");
    
  2. Disconnected Graphs: Unreachable nodes remain at ∞

  3. Duplicate Edges: Always keep minimum weight:

    graph.computeIfAbsent(u, k -> new HashMap<>())
         .merge(v, weight, Math::min);
    
  4. Self-Loops: Skip during neighbor iteration

    if (edge.to == u) continue;
    
  5. 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

  1. 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;
        }
    }
    
  2. Network Routing:

    // OSPF-style cost calculation
    int cost = latency * (1.0 / reliability) * (REFERENCE_BANDWIDTH / bandwidth);
    
  3. 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:

  1. Non-negative weights are essential for correctness
  2. Priority queue choice dramatically affects performance
  3. State space expansion handles constrained problems
  4. Bidirectional search optimizes single-pair queries
  5. 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.