Shortest Paths

intermediate35 min

Choosing the algorithm

Four algorithms cover essentially every shortest-path problem. Which one you need is decided by the edge weights and the number of sources — not by preference.

Which shortest-path algorithm

SituationAlgorithmCost
All edges weight 1BFSO(V + E)
All edges weight 0 or 10-1 BFS with a dequeO(V + E)
Non-negative weights, one sourceDijkstra with a heapO(E log V)
Negative weights allowed, one sourceBellman–FordO(V · E)
All pairs, small graphFloyd–WarshallO(V3)

The first row is worth emphasising: if every edge costs the same, Dijkstra is unnecessary overhead. Plain BFS is simpler and faster. See Graph Representation and Traversal.

Dijkstra

For non-negative weights from a single source. Repeatedly settle the closest unsettled node, then relax its outgoing edges.

static class Edge {
    int to;
    long weight;
    Edge(int to, long weight) { this.to = to; this.weight = weight; }
}

static long[] dijkstra(List<List<Edge>> adj, int source, int n) {
    long[] dist = new long[n + 1];
    Arrays.fill(dist, Long.MAX_VALUE);
    dist[source] = 0;

    // {node, distance}, ordered by distance
    PriorityQueue<long[]> pq =
        new PriorityQueue<>((x, y) -> Long.compare(x[1], y[1]));
    pq.add(new long[]{source, 0});

    while (!pq.isEmpty()) {
        long[] top = pq.poll();
        int u = (int) top[0];
        long d = top[1];

        if (d > dist[u]) continue;      // a stale copy — already settled better

        for (Edge e : adj.get(u)) {
            long nd = d + e.weight;
            if (nd < dist[e.to]) {
                dist[e.to] = nd;
                pq.add(new long[]{e.to, nd});
            }
        }
    }
    return dist;
}

The stale-entry check is not optional

if (d > dist[u]) continue;

Java’s PriorityQueue has no decrease-key operation, so the standard approach pushes a new entry whenever a distance improves and leaves the old one in the heap. That line discards the outdated copies.

Without it the algorithm still terminates with correct distances, but it re-expands nodes repeatedly and the runtime degrades badly on dense graphs.

Other details that matter:

  • dist is long and initialised to Long.MAX_VALUE. Using Integer.MAX_VALUE with int overflows the moment you add a weight to it.
  • Test nd < dist[e.to] before assigning, not after.
  • Unreachable nodes keep Long.MAX_VALUE. Check for that before printing rather than printing a huge number.

Dijkstra requires non-negative weights. With a negative edge it can settle a node too early and never revisit it, producing a wrong answer with no error.

0-1 BFS

When every weight is 0 or 1, a deque replaces the heap and the log factor disappears. Push zero-weight moves to the front and one-weight moves to the back, which keeps the deque sorted by distance automatically.

static int[] zeroOneBfs(List<List<Edge>> adj, int source, int n) {
    int[] dist = new int[n + 1];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[source] = 0;

    Deque<Integer> dq = new ArrayDeque<>();
    dq.addFirst(source);

    while (!dq.isEmpty()) {
        int u = dq.pollFirst();
        for (Edge e : adj.get(u)) {
            int nd = dist[u] + (int) e.weight;
            if (nd < dist[e.to]) {
                dist[e.to] = nd;
                if (e.weight == 0) dq.addFirst(e.to);
                else               dq.addLast(e.to);
            }
        }
    }
    return dist;
}

This turns up in grid problems where some moves are free and others cost one — for example, walking is free but breaking a wall costs one.

Bellman–Ford

Handles negative weights. Relax every edge V - 1 times; any distance that can still improve on an extra pass lies on a negative cycle.

static long[] bellmanFord(int[][] edges, int source, int n) {
    long[] dist = new long[n + 1];
    Arrays.fill(dist, Long.MAX_VALUE / 4);   // headroom so adding cannot overflow
    dist[source] = 0;

    for (int iter = 0; iter < n - 1; iter++) {
        boolean changed = false;
        for (int[] e : edges) {              // {from, to, weight}
            if (dist[e[0]] + e[2] < dist[e[1]]) {
                dist[e[1]] = dist[e[0]] + e[2];
                changed = true;
            }
        }
        if (!changed) break;                 // settled early
    }
    return dist;
}

Two practical notes. Initialising to Long.MAX_VALUE / 4 rather than Long.MAX_VALUE means dist[from] + weight cannot overflow for an unreachable node. And the changed flag often ends the loop far before V - 1 iterations.

To detect a negative cycle, run one more pass; if anything still improves, a negative cycle is reachable from the source.

Floyd–Warshall

All pairs at once, for small graphs. Three nested loops, with the intermediate node outermost.

static void floydWarshall(long[][] dist, int n) {
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }
}

The loop order is the algorithm

k must be the outermost loop. The invariant is that after iteration k, dist[i][j] is the shortest path using only nodes 1..k as intermediates. Putting k inside breaks that invariant and yields wrong answers on some graphs while looking correct on others.

O(V3) means V ≤ 500 comfortably, and V = 1000 is 109 operations — too slow in Java. Above that, run Dijkstra from each node instead.

Initialise the diagonal to 0, absent edges to a large sentinel, and use a sentinel with headroom so the addition cannot overflow.

Reconstructing the path

Distances alone are often not the answer. Record where each improvement came from.

int[] parent = new int[n + 1];
Arrays.fill(parent, -1);

// inside the relaxation:
if (nd < dist[e.to]) {
    dist[e.to] = nd;
    parent[e.to] = u;
    pq.add(new long[]{e.to, nd});
}

// afterwards, walk backwards from the target
List<Integer> path = new ArrayList<>();
for (int at = target; at != -1; at = parent[at]) {
    path.add(at);
}
Collections.reverse(path);

Common mistakes

  • Dijkstra with negative edges. Silently wrong; use Bellman–Ford.
  • Missing the stale-entry check, which makes Dijkstra far slower than its bound.
  • int distances that overflow when a weight is added to the sentinel.
  • Floyd–Warshall with k not outermost.
  • Printing the sentinel for an unreachable node instead of the required marker.
  • Using Dijkstra when all weights are equal — BFS is simpler and faster.
  • Building the adjacency list one-directional for an undirected graph.

Practice

Name the algorithm from the constraints before writing anything.

  1. Unweighted graph, one source, print distances to all nodes.
  2. Weighted graph with positive weights, up to 200,000 nodes and edges, one source.
  3. Graph with possibly negative weights, 1,000 nodes, detect whether a negative cycle exists.
  4. 300 nodes, print the shortest distance between every pair.
  5. Grid where moving to an adjacent cell is free along a road and costs 1 off-road; find the cheapest route across.
Hints
  1. BFS.
  2. Dijkstra with a heap. long distances.
  3. Bellman–Ford, then one extra relaxation pass — if anything improves, a negative cycle is reachable. To find cycles anywhere in the graph rather than only those reachable from one source, initialise every distance to 0.
  4. Floyd–Warshall; 3003 is 2.7 × 107.
  5. 0-1 BFS with a deque.

Related