Sparround

Shortest path: Dijkstra

BFS finds the shortest path in an unweighted graph because there "shortest" = "fewest steps". The moment weights appear, that equality falls apart.

A simple counter-example: there are two routes from A to C — the direct edge A → C (weight 10) and the path A → B → C (weight 1 + 1 = 2). BFS picks the one-step direct edge and returns 10, while the real shortest path costs 2. BFS counts steps, not cost.

So why not "split every edge into weight pieces and run BFS anyway"? In theory you can, but a weight of 1000 turns one edge into 1000 artificial nodes — it does not survive contact with real data.

The right answer is Dijkstra's algorithm: it visits neighbours in order of "cheapest so far" rather than "fewest steps". That requires a priority queue (a min-heap) instead of a plain queue — you always pop the node with the smallest current distance.

Dijkstra rests on three ideas.

1. The `dist` table — for each node, "the cheapest distance from the source found so far". Initially the source is 0 and everything else is infinity.

2. Relaxation — looking from u at v you ask: "is dist[u] + weight(u,v) smaller than the current dist[v]?" If it is, you update dist[v] and push v into the priority queue. The whole algorithm is that one line repeated.

3. The greedy invariant — all of the correctness hangs on this: the distance of a node popped from the priority queue is already final and will never improve. The reason: every remaining route starts from a distance at least as large, and since weights are non-negative a path can only get longer.

Two practical details that must be visible in the code:

  • Stale entries: the same node can enter the queue several times because a plain heap has no "decrease-key". So if a popped entry's distance is larger than the one in dist, you simply drop it (if (d > dist[u]) continue). This is called lazy deletion, and interviewers do look for that line
  • Returning the path itself: if you need the route and not just the cost, keep a prev[v] = u table and walk back from the target at the end

Complexity: with a binary heap O((V + E) log V), usually written O(E log V). Memory O(V).

Negative weights break Dijkstra, and this is the most frequent follow-up question.

The reason lies in the greedy invariant: the moment Dijkstra pops a node it declares its distance final. With a negative edge, a longer path discovered later can reduce that distance — but the algorithm never revisits the node. The result is silently wrong; nothing throws, which is what makes it dangerous.

Counter-example: A → B weight 5, A → C weight 2, C → B weight −4. Dijkstra may finalise C at 2 and then B at 5, while the true answer is 2 + (−4) = −2.

For that case you use Bellman-Ford: it relaxes every edge V − 1 times, at O(V × E) — considerably slower than Dijkstra, but correct with negative weights. It also detects a negative cycle if any improvement is still possible on the V-th iteration (in which case "shortest path" stops being meaningful, since each lap around the cycle lowers the cost).

One clarification: negative weights don't always mean an exotic problem — currency arbitrage, energy balances and profit/cost models are real examples.

AlgorithmWhen it is usedComplexityLimitation
BFSNo weights (or all equal)O(V + E)Gives a wrong answer with weights
DijkstraNon-negative weights, single source to allO((V + E) log V) — with a binary heapSilently incorrect with negative weights
Bellman-FordNegative weights present; need negative-cycle detectionO(V × E)Considerably slower
Floyd-WarshallAll-pairs distances on a small graphO(V³)Not applicable as V grows
A*One specific target plus a good heuristic (maps, games)Faster than Dijkstra in practiceThe heuristic must be admissible or the result isn't optimal

Interview tip: you are not expected to have Dijkstra memorised — you are expected to explain why it works. Three sentences are enough: "The priority queue hands me the cheapest node; when I pop it, its distance is final, because every remaining route starts from a larger distance and no weight is negative; so each node is finalised exactly once." If you can say that, forgetting a detail of the code is fine. If you can't, perfect code won't save you — the next question is always "what changes with a negative weight?"