10. Cheapest Paths

Dijkstra

Shortest path between source node and all other nodes in a graph with no negative edges.

How it works: Always look at the node with the lowest distance from the start node. Repeat this until all nodes have been relaxed (or shortcut once your goal node is visited).

Why it works: All other unvisited nodes must have an optimal distance as high (if edge has weight ) or higher than the lowest unvisited node. You cannot make up distance without neg. edges (triangle inequality).

public static int dijkstra(int s, int t, int n, int[][] E, int[][] W) {
    int[] dist = new int[n];
    
    for (int i=0; i<n; i++) dist[i] = Integer.MAX_VALUE / 2;
    
    record Key(int node, int dist) {}
    Queue<Key> pq = new PriorityQueue<>(
        (a, b) -> Integer.compare(a.dist, b.dist)
    );
	
    dist[s] = 0;
    pq.add(new Key(s, 0));
    
    while (!pq.isEmpty()) {
        Key curr = pq.poll();
        
        if (curr.dist != dist[curr.node]) continue;
        if (curr.node == t) break;
        
        for (int i=0; i<E[curr.node].length; i++) {
            int adjNode = E[curr.node][i];
            int newDist = curr.dist + W[curr.node][i];
            if (newDist < dist[adjNode]) {
                dist[adjNode] = newDist;
                pq.add(new Key(adjNode, newDist));
	        }
        }
    }
    
    return dist[t];
}

Runtime Binary Heap

  • You iterate over all nodes and always extract the min from the binary heap
  • Each time you iterate over a node, you iterate over all incident edges. That equals all edges in total. In the worst case, you have to decrease the key of all edges (or insert a duplicate key), which costs (updating the dist array is ) .

Total:

Runtime Standard Array

While there are still unvisited nodes (totals iterations):

  • You first iterate over all distances to find the min
  • You then iterate over all incident edges to decrease their entry in the distances array. That equals all edges across all runs.

Total:

Dijkstra can also be impl. using a Fibonacci Heap in since decrease-key is amortized . However, it rarely used due to its high constant factors, cache inefficiency, and complex implementation.

Aside: A*

A* is Dijkstra + a heuristic function to estimate the remaining cost from node to the goal. It typically expands fewer nodes than Dijkstra by prioritizing directions that “look” promising.

Instead of selecting the node with the smallest , A* selects the node minimizing . When for all nodes, A* reduces to Dijkstra. We stop once we found the target.

If is admissible (never overestimates the true remaining cost) and consistent ( for every edge), A* is guaranteed to find the optimal shortest path: all nodes coming after must be further away since going through all others must take longer (heuristic is lower bound).

Bellman-Ford

Shortest path between source node and all other nodes in a graph with negative edges.

How it works: Go over all nodes. Relax all adjacent nodes. Repeat times or until no improvement can be made.

Why this works: The shortest path between the start node and any other node can have at most edges. As a shortest path, there exists no shorter path to any of the nodes on the way — otherwise, the path could be abbreviated. Since it starts at the source, the path is extended by one edge every cycle.

Negative cycles: negative cycle any node’s distance improves between and iterations.

Runtime

  • We iterate times
  • Each time, we iterate over all edges, always by iterating over all nodes and looking at the adjacent ones.

Total:

Floyd-Warshall

Shortest path between every node and every other nodes in a graph with negative edges. DP algorithm.

State: = length of path from to using the first nodes as possible intermediaries.

Base case:

Recurrence: At each step, we check if routing through a new intermediary node results in a better result.

Calculation order: Since every entry relies on entries that may be in anywhere in the -th layer, we must compute the entire -th before the -th layer. In practice, this involves nesting the and loops inside the loop.

Optimization: Our recurrence only relies on the -th layer. We can therefore optimize our space usage by updating our recurrence: . We keep the order of the three for loops.

and corresponds to and b/c, by definition, a path starting or ending in cannot use as intermediary node; it wouldn’t be a path then.

Runtime

  • You iterate over with 3 nested loops
  • Each time, you find the minimum and update an array entry in

Total:

If there ex. no negative edges and , running Dijkstra times is faster: vs.

Johnson

Shortest path between every node and every other nodes in a graph with negative edges.

How it works:

  • Create a supernode
  • Create an edge from to all other nodes of weight
  • Compute distance from to every other node using Bellman Ford
  • Update every weight to
  • Run Dijkstra from every node to every other node

Why it works:

  • Updated weights are pos The triangle inequality must hold all are positive
  • Path lengths stay the same The shortest path from to using is the same as using b/c every total path weight equals the sum of the original weights along the path plus . All the other heights cancel out.

Runtime

  • for creating supernode
  • for creating 0-edges from
  • for Bellman Ford
  • for updating all weights
  • for running Dijkstra

Total: