Shortest Path Algorithms


Overview

Choose a shortest-path algorithm from the graph’s edge weights and whether the problem asks for one source or all pairs:

AlgorithmUse it whenTimeSpace
BFSThe graph is unweighted, or every edge has the same weightO(V + E)O(V)
DijkstraEdge weights are nonnegativeO((V + E) log V) with a min-heapO(V + E)
Bellman-FordNegative edge weights may existO(VE)O(V)
Floyd-WarshallAll-pairs shortest paths are needed and the graph is smallO(V³)O(V²)

BFS is covered in the breadth-first search post, so this post focuses on the other three algorithms. Dijkstra cannot safely finalize distances when negative edges exist. Bellman-Ford can detect a reachable negative cycle with one extra relaxation pass. Floyd-Warshall supports negative edges, but shortest paths are undefined when a negative cycle is usable.

For Dijkstra, define the state before writing the algorithm. The state is normally just node, but constraints may expand it to (node, flightsUsed) or (node, discountsUsed). The distance table and heap must store the entire state. These expanded problems use the same algorithm, but their complexity depends on the number of expanded states and transitions rather than only the original V and E.

Templates

Dijkstra

  • Define the complete state. Use one best distance per node normally, or a 2D table when a resource such as flights or discounts is part of the state.
  • Set the starting state’s cost to zero and add it to the min-heap.
  • Pop the cheapest state. Skip it only when cost > best[state]; using >= would incorrectly skip its current best entry.
  • Compute the transition cost. Ordinary shortest paths use cost + weight; minimax paths such as 1631 use max(cost, weight).
  • Store and push a neighbor only when newCost < best[nextState].
  • With nonnegative transitions, the first non-stale destination state removed from the heap is optimal.
import java.util.*;

class Dijkstra {
    public Map<Integer, Integer> shortestPaths(Map<Integer, List<int[]>> adjList, int source) {
        Map<Integer, Integer> best = new HashMap<>();
        best.put(source, 0);
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        minHeap.offer(new int[]{0, source}); // cost, node

        while (!minHeap.isEmpty()) {
            int[] curr = minHeap.poll();
            int cost = curr[0], node = curr[1];
            if (cost > best.getOrDefault(node, Integer.MAX_VALUE)) continue;

            for (int[] edge : adjList.getOrDefault(node, new ArrayList<>())) {
                int nei = edge[0], weight = edge[1];
                int newCost = cost + weight;
                if (newCost < best.getOrDefault(nei, Integer.MAX_VALUE)) {
                    best.put(nei, newCost);
                    minHeap.offer(new int[]{newCost, nei});
                }
            }
        }
        return best;
    }
}

Bellman-Ford

  • Relax every edge up to V - 1 times; after round i, paths using at most i edges are known.
  • Stop early if a complete round makes no changes.
  • If an edge can still be relaxed afterward, a reachable negative cycle exists.
import java.util.Arrays;

class BellmanFord {
	public long[] shortestPaths(int n, int[][] edges, int source) {
		long infinity = Long.MAX_VALUE / 4;
		long[] dist = new long[n];
		Arrays.fill(dist, infinity);
		dist[source] = 0;

		for (int i = 0; i < n - 1; i++) {
			boolean changed = false;
			for (int[] edge : edges) {
				int u = edge[0], v = edge[1], weight = edge[2];
				if (dist[u] != infinity && dist[u] + weight < dist[v]) {
					dist[v] = dist[u] + weight;
					changed = true;
				}
			}
			if (!changed) break;
		}

		for (int[] edge : edges)
			if (dist[edge[0]] != infinity && dist[edge[0]] + edge[2] < dist[edge[1]]) return null;
		return dist;
	}
}

Floyd-Warshall

  • Initialize a distance matrix with zero on the diagonal and direct-edge weights elsewhere.
  • For an undirected graph, store each direct edge in both directions.
  • For each intermediate node k, try every route i -> k -> j.
  • A negative diagonal value after processing indicates a negative cycle.
import java.util.Arrays;

class FloydWarshall {
	public long[][] shortestPaths(int n, int[][] edges) {
		long infinity = Long.MAX_VALUE / 4;
		long[][] dist = new long[n][n];
		for (int i = 0; i < n; i++) {
			Arrays.fill(dist[i], infinity);
			dist[i][i] = 0;
		}
		for (int[] edge : edges)
			dist[edge[0]][edge[1]] = Math.min(dist[edge[0]][edge[1]], edge[2]);

		for (int k = 0; k < n; k++)
			for (int i = 0; i < n; i++)
				for (int j = 0; j < n; j++)
					if (dist[i][k] != infinity && dist[k][j] != infinity)
						dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
		return dist;
	}
}

Problems

743. Network Delay Time

Find how long a signal from one node takes to reach every node in a directed graph with positive travel times. Dijkstra computes the earliest arrival at each node; the maximum finite distance is the total delay.

import java.util.*;

class Solution {
    public int networkDelayTime(int[][] times, int n, int k) {
        Map<Integer, List<int[]>> adjList = new HashMap<>();

        for (int[] time : times){
            int u = time[0], v = time[1], w = time[2];
            adjList.computeIfAbsent(u, ignored -> new ArrayList<>()).add(new int[]{v, w});
        }

        Map<Integer, Integer> nodeToTime = new HashMap<>();
        nodeToTime.put(k, 0);
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        minHeap.offer(new int[]{0, k}); //time, node

        while (!minHeap.isEmpty()){
            int[] curr = minHeap.poll();
            int dist = curr[0], node = curr[1];

            if (dist > nodeToTime.getOrDefault(node, Integer.MAX_VALUE)){
                continue;
            }
            for (int[] pair : adjList.getOrDefault(node, new ArrayList<>())){
                int newNode = pair[0], weight = pair[1];
                int newDist = dist + weight;
                if (newDist < nodeToTime.getOrDefault(newNode, Integer.MAX_VALUE)){
                    nodeToTime.put(newNode, newDist);
                    minHeap.offer(new int[]{newDist, newNode});
                }
            }
        }
        if (nodeToTime.size() == n){
            return Collections.max(nodeToTime.values());
        } else {
            return -1;
        } 
    }
}

1631. Path With Minimum Effort

Treat each cell as a node and the absolute height difference between adjacent cells as an edge cost. A path’s effort is its largest edge cost, so Dijkstra relaxes a neighbor with max(currentEffort, heightDifference) instead of adding the edge cost.

import java.util.Arrays;
import java.util.PriorityQueue;

class Solution {
    public int minimumEffortPath(int[][] heights) {
        int R = heights.length;
        int C = heights[0].length;

        int[][] dist = new int[R][C];
        for (int[] row : dist) {
            Arrays.fill(row, Integer.MAX_VALUE);
        }
        dist[0][0] = 0;
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        minHeap.offer(new int[]{0, 0, 0}); // effort, row, col
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

        while (!minHeap.isEmpty()) {
            int[] curr = minHeap.poll();
            int effort = curr[0], row = curr[1], col = curr[2];

            if (effort > dist[row][col]) {
                continue;
            }

            for (int[] dir : dirs) {
                int nextR = row + dir[0];
                int nextC = col + dir[1];
                if (nextR >= 0 && nextR < R && nextC >= 0 && nextC < C) {
                    int diff = Math.abs(heights[row][col] - heights[nextR][nextC]);
                    int newEffort = Math.max(effort, diff);
                    if (newEffort < dist[nextR][nextC]) {
                        minHeap.offer(new int[]{newEffort, nextR, nextC});
                        dist[nextR][nextC] = newEffort;
                    }
                }
            }
        }

        return dist[R - 1][C - 1];
    }
}

787. Cheapest Flights Within K Stops

Find the cheapest route using at most k intermediate stops, meaning at most k + 1 flights. Treat (city, flightsUsed) as the Dijkstra state because reaching the same city after a different number of flights represents a different route. The first destination state removed from the min-heap has the cheapest valid cost.

import java.util.*;

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        Map<Integer, List<int[]>> graph = new HashMap<>();
        for (int[] flight : flights){
            int u = flight[0];
            int v = flight[1];
            int p = flight[2];
            graph.computeIfAbsent(u, key -> new ArrayList<>()).add(new int[]{v, p});
        }

        int maxFlights = 1 + k;
        int INF = 1000000000;
        int[][] costs = new int[n][1 + maxFlights];
        for (int i = 0; i < n; i++) {
            Arrays.fill(costs[i], INF);
        }
        costs[src][0] = 0; // should have it for consistency

        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        minHeap.offer(new int[]{0, src, 0}); //cost, node, flight

        while (!minHeap.isEmpty()){
            int[] curr = minHeap.poll();
            int cost = curr[0], city = curr[1], count = curr[2];
            // must be > (>= is wrong!)
            if (cost > costs[city][count]){
                continue;
            }
            if (city == dst){
                return cost;
            }
            for (int[] edge : graph.getOrDefault(city, new ArrayList<>())) {
                int newCity = edge[0];
                int price = edge[1];

                if (count < maxFlights) {
                    int newCost = cost + price;
                    int newCount = count + 1;
                    // must be < (<= introduces extra work)
                    if (newCost < costs[newCity][newCount]) {
                        costs[newCity][newCount] = newCost;
                        minHeap.offer(new int[]{newCost, newCity, newCount});
                    }
                }
            }
        }
        return -1;
    }
}

1334. Find the City With the Smallest Number of Neighbors at a Threshold Distance

Compute all-pairs shortest distances, count how many other cities each city can reach within the threshold, and return the largest city index on ties. Floyd-Warshall tries every city as an intermediate point in O(n³) time.

import java.util.Arrays;

class Solution {
    public int findTheCity(int n, int[][] edges, int distanceThreshold) {
        int INF = 1000000000;
        int[][] dist = new int[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(dist[i], INF);
            dist[i][i] = 0;
        }

        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], w = edge[2];
            dist[u][v] = w;
            dist[v][u] = w;
        }

        // Intermediate node must be the outer loop.
        for (int k = 0; k < n; k++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    dist[i][j] = Math.min(dist[i][j], dist[i][k] + dist[k][j]);
                }
            }
        }

        int minCount = n;
        int res = -1;
        for (int r = 0; r < n; r++) {
            int count = 0;
            for (int c = 0; c < n; c++) {
                if (r != c && dist[r][c] <= distanceThreshold) {
                    count++;
                }
            }
            if (count <= minCount) {
                minCount = count;
                res = r;
            }
        }

        return res;
    }
}

2093. Minimum Cost to Reach City With Discounts

Find the minimum highway cost when a limited number of tolls may be halved. Run Dijkstra over (city, discountsUsed) states. Every highway produces a full-price transition and, when available, a discounted transition.

import java.util.*;

class Solution {
    public int minimumCost(int n, int[][] highways, int discounts) {
        Map<Integer, List<int[]>> graph = new HashMap<>();

        for (int[] highway : highways) {
            int city1 = highway[0], city2 = highway[1], toll = highway[2];
            graph.computeIfAbsent(city1, key -> new ArrayList<>()).add(new int[]{city2, toll});
            graph.computeIfAbsent(city2, key -> new ArrayList<>()).add(new int[]{city1, toll});
        }

        // best[node][discountsUsed] is the best cost seen so far.
        int INF = 1000000000;
        int[][] best = new int[n][1 + discounts];
        for (int[] row : best) {
            Arrays.fill(row, INF);
        }
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        minHeap.offer(new int[]{0, 0, 0}); // costToNode, discountsUsed, node

        while (!minHeap.isEmpty()) {
            int[] curr = minHeap.poll();
            int costToNode = curr[0], discountsUsed = curr[1], node = curr[2];

            if (node == n - 1) {
                return costToNode;
            }
            if (costToNode > best[node][discountsUsed]) {
                continue;
            }

            for (int[] pair : graph.getOrDefault(node, new ArrayList<>())) {
                int nei = pair[0], toll = pair[1];
                int newCost = costToNode + toll;
                if (newCost < best[nei][discountsUsed]) {
                    best[nei][discountsUsed] = newCost;
                    minHeap.offer(new int[]{newCost, discountsUsed, nei});
                }

                if (discountsUsed + 1 <= discounts) {
                    int newCost2 = costToNode + toll / 2;
                    if (newCost2 < best[nei][1 + discountsUsed]) {
                        best[nei][1 + discountsUsed] = newCost2;
                        minHeap.offer(new int[]{newCost2, 1 + discountsUsed, nei});
                    }
                }
            }
        }

        return -1;
    }
}