The Story That Explains Bellman-Ford
Bellman-Ford is the patient, thorough planner. It doesn't commit early. Instead it re-checks every single road, over and over, letting better prices ripple through the map. After enough full sweeps, every city shows its true cheapest cost — refunds included. And if it ever finds a loop that keeps getting cheaper forever, it raises a flag: a negative cycle.
Bellman-Ford finds shortest paths from one source to every node in a weighted, directed graph — and unlike Dijkstra, it works even when some edge weights are negative. Its method is brute-force but bulletproof: relax every edge, V−1 times.
To "relax" edge u → v with weight w: if dist[u] + w < dist[v],
update dist[v]. Bellman-Ford simply does this for all edges, then does it
again, and again — V−1 full passes. No priority queue, no greedy choice.
Each pass sweeps through every edge again — better distances ripple one hop further each time.
The Three Phases
In a graph with V nodes and no negative cycle, the shortest path between any two nodes uses at most V−1 edges (more than that would repeat a node). Each pass pushes correct distances one more edge along every path, so after V−1 passes the longest possible shortest path is fully resolved.
The Graph We'll Use
Four nodes (A–D), source A, a directed graph with one negative edge (B → C = −2). The edges are processed in the order below — deliberately "awkward" so you can watch distances need several passes to settle.
| # | Edge | Weight |
|---|---|---|
| 1 | C → D | 3 |
| 2 | B → C | −2 |
| 3 | A → B | 4 |
| 4 | A → C | 5 |
| 5 | B → D | 10 |
Hands-On: Watch Each Line Run
The heart of the tutorial. Press Play (or Step) and watch each edge light up amber as it's tested, the target node flash green when its distance improves, and the distance labels fall pass after pass. The edge strip shows progress through the current pass, and the exact Python line running lights up on the right.
def bellman_ford(graph, edges, src):
dist = {v: float('inf') for v in graph}
dist[src] = 0
for i in range(len(graph) - 1):
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
for u, v, w in edges:
if dist[u] + w < dist[v]:
raise ValueError('negative cycle')
return dist
Amber edge / node = being relaxed right now. Green node = its distance just improved. Green edge = part of the current shortest-path tree. Indigo node = reached (finite distance); slate = still ∞. The gold ring marks the source.
The Code, Line by Line — "This Line Does That"
Same program as the animation, fully annotated. Each line maps to a job the visualiser highlights.
def bellman_ford(graph, edges, src):
dist = {v: float('inf') for v in graph} # all nodes unknown
dist[src] = 0 # source is 0 from itself
for i in range(len(graph) - 1): # V-1 full passes
for u, v, w in edges: # try to relax EVERY edge
if dist[u] + w < dist[v]: # found a cheaper path?
dist[v] = dist[u] + w # relax: update distance
for u, v, w in edges: # one extra pass = detector
if dist[u] + w < dist[v]: # still improving?
raise ValueError('negative cycle') # no shortest path
return dist # shortest distance to each node
def bellman_ford(graph, edges, src) — takes the node set, the edge list (u, v, w), and the source.
dist = {v: float('inf') ...} — Phase 1: every distance starts at ∞.
dist[src] = 0 — the source is zero from itself.
for i in range(len(graph) - 1) — Phase 2: repeat the relaxation sweep V−1 times.
for u, v, w in edges — visit every edge in the graph this pass.
if dist[u] + w < dist[v] — the relaxation test: is going through u cheaper?
dist[v] = dist[u] + w — relax the edge and record the better distance.
for u, v, w in edges — Phase 3: one final sweep to check for trouble.
if dist[u] + w < dist[v] — if anything still improves, distances never settled.
raise ValueError('negative cycle') — report it: no finite shortest path exists.
return dist — the shortest distance from the source to every node.
Full Working — Every Edge, Every Pass
Here is the complete trace from source A. Watch how node D needs all three passes to reach its true value (14 → 8 → 5), because its shortest path A→B→C→D has three edges — one resolved per pass. skip (∞) means the source end was still unknown.
| Pass | Edge | Test | Action | Distances after |
|---|---|---|---|---|
| 1 | C→D (3) | ∞+3 < ∞ | skip | A0 B∞ C∞ D∞ |
| 1 | B→C (−2) | ∞−2 < ∞ | skip | A0 B∞ C∞ D∞ |
| 1 | A→B (4) | 0+4 < ∞ ✓ | B = 4 | A0 B4 C∞ D∞ |
| 1 | A→C (5) | 0+5 < ∞ ✓ | C = 5 | A0 B4 C5 D∞ |
| 1 | B→D (10) | 4+10 < ∞ ✓ | D = 14 | A0 B4 C5 D14 |
| 2 | C→D (3) | 5+3=8 < 14 ✓ | D = 8 | A0 B4 C5 D8 |
| 2 | B→C (−2) | 4−2=2 < 5 ✓ | C = 2 | A0 B4 C2 D8 |
| 2 | A→B (4) | 0+4=4 < 4 | no | A0 B4 C2 D8 |
| 2 | A→C (5) | 0+5=5 < 2 | no | A0 B4 C2 D8 |
| 2 | B→D (10) | 4+10=14 < 8 | no | A0 B4 C2 D8 |
| 3 | C→D (3) | 2+3=5 < 8 ✓ | D = 5 | A0 B4 C2 D5 |
| 3 | B→C (−2) | 4−2=2 < 2 | no | A0 B4 C2 D5 |
| 3 | A→B, A→C, B→D | all ≥ | no | A0 B4 C2 D5 |
| check | all 5 edges | none improve | no negative cycle ✓ | A0 B4 C2 D5 |
B = 4 (A→B), C = 2 (A→B→C, using the −2 edge), D = 5 (A→B→C→D). Notice C's best route goes through B even though A→C directly costs 5 — the negative edge made the longer hop cheaper. That's exactly the case Dijkstra would get wrong.
Detecting a Negative Cycle
A negative cycle is a loop whose edge weights sum to less than zero. Going around it keeps lowering the total forever, so no shortest path exists. After V−1 passes distances should be final — if a V-th pass still improves something, a negative cycle is present.
X→Y→Z→X sums to 1 + (−3) + 1 = −1 < 0 — a negative cycle.
Lines 8–10 are the whole detector. Run one more relaxation sweep after the V−1 passes.
Because distances should already be final, any further improvement is impossible unless a
negative cycle keeps feeding the system — so we raise and report it.
Bellman-Ford vs Dijkstra
| Property | Value |
|---|---|
| Negative edges | Yes ✓ |
| Detects neg. cycle | Yes ✓ |
| Strategy | Relax all, V−1× |
| Time | O(V·E) |
| Speed | Slower |
| Property | Value |
|---|---|
| Negative edges | No |
| Detects neg. cycle | No |
| Strategy | Greedy + heap |
| Time | O((V+E) log V) |
| Speed | Faster |
Dijkstra is faster but assumes non-negative weights. Bellman-Ford is slower but robust: it tolerates negative edges and tells you when no answer exists. Use Dijkstra by default; reach for Bellman-Ford the moment negative weights enter the picture.
Time & Space Complexity
| Case | When | Complexity |
|---|---|---|
| Time (typical) | V−1 passes × E edges | O(V · E) |
| Time (best, early-exit) | No change in a pass → stop | O(E) |
| Space | One distance per node | O(V) |
| Negative edges | Supported | Yes ✓ |
| Negative cycle | Detected, not solved | Reported |
Track whether any edge relaxed during a pass. If a full pass makes zero changes, distances have converged — you can stop early instead of finishing all V−1 passes. In our example, that would still need all 3 passes, but on many graphs it ends much sooner.
Benefits
When To Use It — and When Not To
Where Bellman-Ford Sits Among Path Algorithms
| Algorithm | Solves | Weights | Time | Neg. cycle? |
|---|---|---|---|---|
| BFS | Single-source | Unweighted | O(V + E) | n/a |
| Dijkstra | Single-source | Non-negative | O((V+E) log V) | No |
| Bellman-Ford | Single-source | Any | O(V·E) | Detects ✓ |
| Floyd-Warshall | All-pairs | Any | O(V³) | Detects ✓ |
| Johnson's | All-pairs | Any | O(V² log V + VE) | Detects ✓ |
Unweighted? BFS. Non-negative weights? Dijkstra. Negative edges or need cycle detection? Bellman-Ford. Every pair on a dense graph? Floyd-Warshall. Every pair on a sparse graph with negatives? Johnson's (which runs Bellman-Ford once).