Shortest Path (Dijkstra)

BFS finds the shortest path in an unweighted graph because every edge "costs" the same one step. The moment edges have different weights — flight durations, road distances, network latencies — BFS breaks: a path with more edges can still be cheaper overall than a path with fewer, heavier ones. Dijkstra's algorithm solves shortest path for weighted graphs (as long as no edge weight is negative) by always expanding the closest unvisited node next, using a min-heap to efficiently find that node.

The Problem BFS Can't Solve

A --1-- B
|       |
10      1
|       |
C ------D
   1

BFS from A would report B and C as equally "close" (both one edge away) and never realize that A→B→D→C (cost 1+1+1=3) is far cheaper than A→C directly (cost 10). BFS counts edges, not weights — it has no concept of "cost" at all.

Dijkstra's Algorithm

Keep a dist[] array initialized to infinity (0 for the source). Use a min-heap (Java's PriorityQueue) of (node, distance) pairs, always pulling out the node with the smallest known distance next. For each of its neighbors, if going through the current node gives a shorter path than what's currently recorded, update it — this update step is called relaxation.

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

int[] dijkstra(List<List<Edge>> adj, int source, int V) {
    int[] dist = new int[V];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[source] = 0;

    // min-heap ordered by distance: smallest known distance comes out first
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
    pq.add(new int[]{source, 0});  // {node, distance}

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

        if (d > dist[node]) continue;  // stale entry, a shorter path was already found

        for (Edge edge : adj.get(node)) {
            int newDist = d + edge.weight;
            if (newDist < dist[edge.to]) {
                dist[edge.to] = newDist;                       // relax: found a shorter path
                pq.add(new int[]{edge.to, newDist});
            }
        }
    }
    return dist;
}

Trace-Through on the Example Graph

Using A=0, B=1, C=2, D=3 and edges A-B(1), A-C(10), B-D(1), C-D(1), starting from A:

Initial:        dist = [A:0, B:inf, C:inf, D:inf]     heap: [(A,0)]

Pop (A,0):      relax B: 0+1=1  < inf -> dist[B]=1     heap: [(B,1), (C,10)]
                relax C: 0+10=10 < inf -> dist[C]=10
dist = [A:0, B:1, C:10, D:inf]

Pop (B,1):      relax D: 1+1=2  < inf -> dist[D]=2     heap: [(D,2), (C,10)]
dist = [A:0, B:1, C:10, D:2]

Pop (D,2):      relax C: 2+1=3  < 10 -> dist[C]=3      heap: [(C,3), (C,10)]
dist = [A:0, B:1, C:3, D:2]

Pop (C,3):      no shorter path found through C        heap: [(C,10)]
dist = [A:0, B:1, C:3, D:2]

Pop (C,10):     10 > dist[C]=3 -> stale entry, skipped  heap: []

Final shortest distances from A:  B=1, C=3, D=2

Notice C's shortest distance ends up as 3 (via A→B→D→C), not 10 (the direct A→C edge) — exactly the case a plain BFS-by-edge-count would have missed.

Time complexity: O((V + E) log V) — each of the up to E edge relaxations does a heap insert/removal costing O(log V), and every vertex is popped from the heap at least once. Space complexity: O(V + E) for the adjacency list plus O(V) for the distance array and heap contents.

The Key Limitation: Negative Weights

Dijkstra assumes distances only ever grow as you add edges. Once a node is popped from the heap with its current smallest distance, the algorithm treats that distance as final and never revisits it. A negative-weight edge discovered later could make an already-finalized path cheaper — but Dijkstra has no mechanism to go back and fix it, so it can produce a wrong answer entirely without any error or warning.

For graphs that may contain negative edge weights (but no negative cycles), Bellman-Ford is the standard alternative — it relaxes every edge V-1 times instead of relying on a heap's greedy ordering, which correctly handles negative weights at the cost of a slower O(V·E) runtime. It's not covered in depth here since Dijkstra covers the overwhelming majority of interview shortest-path questions, but it's worth knowing the name and the trade-off it exists for.

Next up: Dijkstra finds the cheapest path between two specific nodes. Minimum Spanning Tree tackles a related but different problem — connecting every node as cheaply as possible.