Google Maps offers two routes to the airport: 3 turns through a jammed flyover, or 7 turns through empty service roads. BFS would pick the first — fewest hops. Every human picks the second — least time. The moment roads have lengths, counting hops stops answering the real question.
This lesson upgrades BFS for weighted roads (Dijkstra), meets the one input that breaks the upgrade (Bellman-Ford to the rescue), and then answers a different question people constantly confuse with routing: wiring everything together at minimum cost (Prim and Kruskal). Four named algorithms, and — notice — every one of them is a greedy algorithm whose safety someone already proved.
Where BFS's guarantee dies
BFS's promise was: first arrival = fewest edges. Add weights and the promise answers the wrong question — a direct road of cost 10 loses to a two-hop detour of 1 + 2, but BFS reaches the destination via the direct road first and confidently records 10. BFS counts hops; weighted graphs need sums. What would need to change in BFS to fix this?
Dijkstra: serve the cheapest, settle it forever
Look at WHERE BFS's guarantee came from: the queue served vertices in distance order. With weights, a plain queue can't do that — but a min-heap can: always serve the vertex with the smallest total-cost-so-far. That single substitution is Dijkstra's algorithm.
Why does serving-cheapest restore the guarantee? When the heap hands you vertex u at cost d, every other route to u would have to pass through something currently costlier — and (with non-negative weights!) grow from there. Nothing can undercut d later, so u is settled: its answer is final. The improving-updates along the way have a name — relaxation: found a cheaper route to v? Lower v's estimate.
import heapq
def dijkstra(adj, src): # adj: u -> [(v, weight), ...]
dist = {src: 0}
heap = [(0, src)]
while heap:
d, u = heapq.heappop(heap)
if d > dist.get(u, float("inf")):
continue # stale entry - skip (see below)
for v, w in adj[u]:
nd = d + w
if nd < dist.get(v, float("inf")):
dist[v] = nd # RELAX: found a cheaper route
heapq.heappush(heap, (nd, v))
return distTwo Python habits carry the implementation: push improved estimates freely (heapq can't update entries in place), and skip stale pops — the d > dist check discards leftovers whose vertex already settled cheaper. That's the lazy-deletion idiom from the heaps lesson, earning its keep. Cost: O((V + E) log V).
Dry run: every relaxation shown
edges: A->B(4), A->C(1), C->B(2), B->D(1), C->D(5)
pop (0, A): relax B->4, C->1 heap=[(1,C),(4,B)]
pop (1, C): relax B: 1+2=3 < 4 -> B=3 ; D: 1+5=6 -> D=6
heap=[(3,B),(4,B),(6,D)]
pop (3, B): relax D: 3+1=4 < 6 -> D=4
heap=[(4,B),(4,D),(6,D)]
pop (4, B): STALE (dist[B]=3) -> skip
pop (4, D): current -> settled
pop (6, D): stale -> skip
dist = {A:0, C:1, B:3, D:4}Read the story in it: the direct A→B edge (4) lost to the detour A→C→B (1+2 = 3), and D's first estimate 6 was undercut to 4 via B. Exactly the airport-route correction BFS couldn't make.
One negative edge breaks everything
Go back to the settling argument and find its quiet assumption: "every other route would grow from there." What if an edge could SHRINK a route — a negative weight? (A cashback leg; a downhill that recharges the battery.) Then a path through expensive-then-negative can undercut a vertex Dijkstra already settled — but settled means we stopped listening. Dijkstra with negative edges doesn't crash — it confidently returns wrong answers, the worst failure mode software has.
"Why does Dijkstra fail on negative weights?" is a top-three reported graph question, and now you own the answer: the greedy settle-forever step assumes extending never cheapens, and negative edges break exactly that.
Bellman-Ford: slower, tougher
If settling early is the vulnerability, don't settle: relax EVERY edge, V−1 times. Why V−1? A shortest path visits no vertex twice, so it has at most V−1 edges — and each full pass locks in all shortest paths one edge longer than the last pass did. After V−1 passes, every shortest path (negatives included) has been found.
def bellman_ford(n, edges, src): # edges: (u, v, w)
INF = float("inf")
dist = [INF] * n
dist[src] = 0
for _ in range(n - 1): # V-1 rounds of relax-everything
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
for u, v, w in edges: # a V-th round: anything STILL improving?
if dist[u] + w < dist[v]:
return None # negative CYCLE: "shortest" undefined
return distPrice: O(V·E) versus Dijkstra's near-linearithmic — robustness costs. Bonus: if a V-th pass still improves something, the graph has a negative cycle — a loop you could ride forever decreasing cost, so "shortest" doesn't exist. Currency-arbitrage detection is literally this check.
A different question: spanning trees
New scenario, different question. Connect 6 villages with optical fibre. Nobody asks how far village A is from village B along the network — only that everything is connected, at minimum total cable cost.
The cheapest connector has a forced shape: it's a tree — any cycle contains a removable edge (the network stays connected without it), so the optimum has none. Connected + acyclic on V vertices means exactly V−1 edges — the tree theorem cashing in. This cheapest connector is the minimum spanning tree (MST).
Shortest path optimises routes between points; MST optimises total connection cost. The MST path between two villages can be long — that's fine; nobody promised routes. Mixing these two questions up is the classic error this topic exists to test.
Prim vs Kruskal
Two greedy strategies, both provably optimal (exchange arguments again), different growth patterns:
Kruskal: sort ALL edges by weight; take each unless it would close a cycle; stop at V−1 taken. A forest of islands merging. And the cycle check? "Would this edge connect two already-connected vertices?" — that is verbatim union-find's question: same root → reject. DSU's promised second algorithm, delivered. O(E log E), dominated by the sort.
Prim: grow ONE tree from any start; keep a min-heap of edges leaving the tree; repeatedly absorb the cheapest outside neighbour. Structurally Dijkstra's twin — same heap loop, one line different: the key is the single edge weight into the tree, not the accumulated distance from a source. O(E log V).
Practical selection: sparse graph, edges easy to sort → Kruskal; dense or adjacency-list-native → Prim. Same total weight either way (edge sets may differ under ties).
What if...?
What if I need the actual route, not just distances? Keep a parent map: whenever relaxation improves dist[v] via u, record parent[v] = u; walk parents backward from the target and reverse. Works identically for BFS and Bellman-Ford.
What if the graph is disconnected? Unreachable vertices simply never enter dist (Dijkstra) or stay INF (Bellman-Ford) — report them as unreachable rather than crashing on a missing key. For MST: no spanning tree exists; Kruskal ends with fewer than V−1 edges — check it.
What if all weights are equal? Then hop-count IS the cost — Dijkstra collapses into an expensive BFS. Use BFS; using Dijkstra anyway is correct but signals you didn't notice.
What if negative edges exist but no negative cycles? Bellman-Ford handles it exactly; Dijkstra stays broken. And if negative cycles exist, no algorithm can define "shortest" — detection (Bellman-Ford's V-th pass) is the only honest answer.
Common mistakes
- Running Dijkstra on graphs with negative edges — wrong answers, no error.
- Confusing MST with shortest paths — different questions, different algorithms, different outputs.
- Trying to update priorities inside heapq — push duplicates and skip stale pops instead.
- Forgetting Kruskal's stop condition (V−1 edges) and scanning on pointlessly.
- Claiming Bellman-Ford "handles" negative cycles — it detects them; nothing can route through one.
- Using Dijkstra on unweighted graphs where BFS suffices.
Choosing the algorithm: the decision table
| Situation | Algorithm | Cost |
|---|---|---|
| Unweighted shortest path | BFS | O(V + E) |
| Weighted, no negative edges | Dijkstra | O((V+E) log V) |
| Negative edges possible | Bellman-Ford | O(V·E) |
| Detect negative cycles | Bellman-Ford's V-th pass | included |
| Connect everything cheaply | Kruskal / Prim (MST) | O(E log E) / O(E log V) |
Choose by constraint, never by speed ranking: the fastest algorithm that tolerates your graph's properties. Saying that sentence, then reading the table, is the complete interview answer.
Quick revision
| Algorithm | Question | Time | Negatives? |
|---|---|---|---|
| BFS | fewest edges | O(V + E) | n/a (unweighted) |
| Dijkstra | cheapest route | O((V+E) log V) | no — settled-is-final breaks |
| Bellman-Ford | cheapest route | O(V·E) | yes + detects negative cycles |
| Prim / Kruskal | cheapest total connection | O(E log V) / O(E log E) | fine (weights just sort) |
One thing to remember
Dijkstra is BFS with a min-heap, and its one blind spot is negative edges — while MST is a different question entirely: connect everything, not route between points.
Practice Zone — PYQs from real selection rounds
Six MCQs, then two full traces: Dijkstra with every heap operation shown, and Kruskal accepting and rejecting edges by hand.
BFS finds shortest paths in unweighted graphs. Dijkstra exists because:
Asked in

Dijkstra's algorithm fails with NEGATIVE edge weights because:
Asked in

A minimum spanning tree of a connected weighted graph is:
Asked in

Kruskal's algorithm sorts edges by weight and adds each unless it creates a cycle. The cycle check uses:
Asked in

Prim vs Kruskal — the practical difference is:
Asked in

For flight prices with occasional CASHBACK legs (negative weights) and a need to detect exploitable money-making loops, you should use:
Asked in

Hands-on tasks:
Graph: A→B (4), A→C (1), C→B (2), B→D (1), C→D (5). Run Dijkstra from A and trace the priority queue and dist map at every pop.
Asked in

Edges (weight): AB(1), BC(4), AC(3), CD(2), BD(5). Build the MST with Kruskal — show each accept/reject and the final total weight.
Asked in

FAQ
Why are Prim and Dijkstra so similar yet answer different questions?
Both grow a region greedily with a min-heap; the difference is one line — the heap key. Dijkstra keys on accumulated distance from the source (routes); Prim keys on the single edge weight into the tree (connection cost). Same machinery, different objective — the precise answer to a favourite trick question.
What about A* — should I know it?
One sentence's worth: Dijkstra plus a heuristic estimate of remaining distance, steering exploration toward the goal — same answer, often far fewer vertices explored. It's the maps/games workhorse; placement rounds rarely go deeper than that sentence.
All-pairs shortest paths — run Dijkstra V times?
That works for sparse graphs. The named alternative is Floyd-Warshall: a triple loop over "does going via k improve i→j?" — O(V³), matrix-shaped, and secretly a DP over allowed intermediate vertices. Know its name, shape and cost; implement on demand.
Why exactly V−1 rounds in Bellman-Ford?
A shortest path repeats no vertex, so it has at most V−1 edges. Round k guarantees all shortest paths using ≤ k edges are final — induction on path length. Fewer rounds can miss long thin paths; more rounds change nothing (that's exactly what the V-th detection pass exploits).
Routes and networks priced. Next, we drop below the structures entirely — numbers as rows of switches — Lesson 11: Bit Manipulation →


