Dijkstra vs Bellman-Ford: When the Greedy Shortcut Fails
7 min readBytePatterns
Both find shortest weighted paths, but only one survives negative edges. The exact moment Dijkstra's greedy promise breaks and how Bellman-Ford avoids it.
Dijkstra and Bellman-Ford answer the same question — the cheapest route from one node to every other on a weighted graph — and most of the time they return identical numbers. The interesting part is the one situation where they do not, because it tells you exactly what Dijkstra is betting on and why the slower algorithm exists at all.
The problem it solves
Once edges carry costs, the fewest-hops answer from breadth-first search stops being the cheapest one. Two routes of three edges can differ wildly in total, and a five-edge route can beat a one-edge route. You need an algorithm that compares sums, not hop counts.
Both algorithms use the same primitive, relaxation: if reaching u costs d and the edge u → v costs w, then v can be reached for d + w, and if that beats what you had for v, you write it down. The two algorithms differ only in which edges they relax, and when they stop.
The intuition
Dijkstra makes a greedy bet. It always expands the unfinished node with the smallest known distance and declares that distance final on the spot. The justification is a one-line argument:
Any other route to this node must leave the settled region through some node that is already at least as far away — and walking further can only add cost.
Read that sentence again and notice the hidden word: add. The argument holds only if no edge can make a route cheaper. One negative edge and a detour that looks longer at the halfway point can finish shorter. Dijkstra has already stamped the node "final" and will not look at it again.
Bellman-Ford refuses to bet. It stamps nothing. It relaxes every edge, in a full sweep, and repeats. After sweep one, every shortest path that uses one edge is correct; after sweep two, every path of up to two edges; and since a shortest path in a graph without negative cycles never repeats a node, it has at most V - 1 edges. So V - 1 sweeps are always enough.
Watch it run
The graph below has one negative edge, b → c at -3. Watch c: the sweep first reaches it directly for 5, and then the relaxation through b pulls it down to 1. Nothing was finalised early, so the cheaper number simply overwrites the older one.
Bellman-Ford
Step 1 of 8
No heap, no ordering. Every node starts at ∞ and the whole edge list will be swept, V-1 = 3 times.
The same interactive animation as the lesson — step through it with the controls.
The same graph, run through Dijkstra, happens to come out right — c is still unfinished when b is expanded. That is the unsettling thing about negative edges: the greedy version is not always wrong, it is unreliably right. The next example makes it fail.
The code
Four nodes. The direct edge a → b costs 2, but the detour a → c → b costs 5 + (-4) = 1. Dijkstra settles b at 2 before it ever expands c.
import heapq
INF = float("inf")
nodes = ["a", "b", "c", "d"]
edges = [("a", "b", 2), ("a", "c", 5), ("b", "d", 1), ("c", "b", -4)]
graph = {n: [] for n in nodes}
for u, v, w in edges:
graph[u].append((v, w))
def dijkstra(src):
dist = {n: INF for n in nodes}
dist[src] = 0
done, pq = set(), [(0, src)]
while pq:
d, u = heapq.heappop(pq)
if u in done:
continue
done.add(u) # the greedy promise: final
for v, w in graph[u]:
if v not in done and d + w < dist[v]:
dist[v] = d + w
heapq.heappush(pq, (dist[v], v))
return dist
def bellman_ford(src, edges):
dist = {n: INF for n in nodes}
dist[src] = 0
for rounds in range(1, len(nodes)): # V - 1 sweeps at most
changed = False
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
changed = True
if not changed: # a quiet sweep: nothing left to learn
break
cycle = any(dist[u] + w < dist[v] for u, v, w in edges)
return dist, rounds, cycle
print(dijkstra("a"))
# {'a': 0, 'b': 2, 'c': 5, 'd': 3} b and d are wrong
print(bellman_ford("a", edges))
# ({'a': 0, 'b': 1, 'c': 5, 'd': 2}, 3, False)
loop = [("a", "b", 1), ("b", "c", 2), ("c", "b", -3), ("c", "d", 1)]
print(bellman_ford("a", loop)[2]) # True: b -> c -> b costs -1 per lap
The error in the Dijkstra output is not only at b. Because b was wrong when it was expanded, d inherited the mistake. A single bad "final" stamp propagates to everything downstream of it.
The final check in bellman_ford is the other half of the algorithm's value. After V - 1 sweeps every true shortest distance is known, so if one more sweep can still improve anything, there is no true shortest distance: some cycle has negative total weight and you can lap it forever. That second graph does exactly that.
The complexity
- Dijkstra with a binary heap:
O((V + E) log V). Each node is settled once; each edge causes at most one push. - Bellman-Ford:
O(V · E). Up toV - 1sweeps, each touching allEedges. The early exit on a quiet sweep helps in practice but does not change the worst case.
On a sparse graph with a hundred thousand nodes and as many edges, the first is a few million steps and the second is ten billion. You pay that price only when you need what Bellman-Ford buys.
Where it goes wrong
- "Just remove the
doneset." The heap-only version, which skips stale entries withif d > dist[u]: continue, does get this small example right, because it re-expandsbwhen the cheaper value arrives. But it has quietly stopped being Dijkstra: nodes can be expanded many times, and on adversarial graphs the running time can blow up exponentially. It is a correctness patch that throws away the complexity guarantee. - "Just add a constant to every weight." Shifting every edge by
+4makes all weights non-negative but penalises routes by their number of edges, so a many-hop route that was cheapest may lose. The reweighting that does work — Johnson's algorithm — uses node potentials computed by one Bellman-Ford run, not a flat constant. - Negative cycles in Dijkstra. It will not detect them; it just returns a number. Only an algorithm that keeps relaxing can notice that relaxing never stops.
- Undirected negative edges. An undirected edge of weight
-1is itself a negative cycle — go across and come back. Most shortest-path questions with negative weights are therefore on directed graphs.
How to say it in an interview
Lead with the assumption, not the algorithm names:
"Dijkstra is greedy: when it pops the nearest unfinished node it declares that distance final, which is only safe because extending a path can never make it cheaper. With non-negative weights I would use it — O((V + E) log V) with a heap. If any weight can be negative, that argument breaks, so I would use Bellman-Ford: relax every edge V - 1 times, O(V · E), and one extra sweep that still improves a distance means a negative cycle."
Then name the practical examples where negative weights show up — rebates on some routes, or currency conversion rates turned into weights with a negative logarithm, where a negative cycle means an arbitrage loop. It shows you know why the slower algorithm is not academic.