Data Structure 📂 Graph Algorithms · 3 of 6 38 min read

Bellman-Ford, Hands-On: Shortest Paths with Negative Weights

An interactive, beginner-friendly Bellman-Ford tutorial in Python where the code and a directed-graph animation run together — every edge is relaxed pass by pass, distance labels update live, and the executing line is highlighted at each step. Covers the toll-refund analogy, the three phases, why V−1 passes, the full edge-by-edge working table, negative-cycle detection, a Dijkstra comparison, complexity, benefits, use cases (currency arbitrage, distance-vector routing), and where it sits among p

Section 01

The Story That Explains Bellman-Ford

The Road Trip With Toll Refunds
Picture a road trip across a country where some roads charge a toll (a cost) but others actually refund you money — a negative cost. You want the cheapest route. A greedy planner like Dijkstra would lock in "the cheapest city reached so far" and never look back — but a surprise refund road could later make a different route cheaper, and Dijkstra has already moved on. It gets the wrong answer.

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.

🔄
The Core Idea: Relax Everything, Repeatedly

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.

Pass 1
Pass 2
Pass 3

Each pass sweeps through every edge again — better distances ripple one hop further each time.


Section 02

The Three Phases

01
Initialise
Source distance = 0, every other node = . We don't yet know how to reach them.
02
Relax All Edges, V−1 Times
Do V−1 full passes. In each pass, try to relax every edge. Distances steadily improve and settle.
03
Detect Negative Cycles
Do one more pass. If any edge can still be relaxed, a negative cycle exists — no shortest path is possible.
📐
Why Exactly V−1 Passes?

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.


Section 03

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.

#EdgeWeight
1C → D3
2B → C−2
3A → B4
4A → C5
5B → D10

Section 04

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.

💰 Live Bellman-Ford — code ↔ animated graph pass 0 / 3
relaxing just updated reached ∞ unreached
Distances from A
Edges this pass
Press Play or Step ▶ to begin. The highlighted line on the right is the one running.
0Pass
0Relaxations
0Edge checks
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
💡
What the colours mean

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.


Section 05

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
🔎 What Each Line Is Responsible For
Line 1
def bellman_ford(graph, edges, src) — takes the node set, the edge list (u, v, w), and the source.
Line 2
dist = {v: float('inf') ...}Phase 1: every distance starts at ∞.
Line 3
dist[src] = 0 — the source is zero from itself.
Line 4
for i in range(len(graph) - 1)Phase 2: repeat the relaxation sweep V−1 times.
Line 5
for u, v, w in edges — visit every edge in the graph this pass.
Line 6
if dist[u] + w < dist[v] — the relaxation test: is going through u cheaper?
Line 7
dist[v] = dist[u] + w — relax the edge and record the better distance.
Line 8
for u, v, w in edgesPhase 3: one final sweep to check for trouble.
Line 9
if dist[u] + w < dist[v] — if anything still improves, distances never settled.
Line 10
raise ValueError('negative cycle') — report it: no finite shortest path exists.
Line 11
return dist — the shortest distance from the source to every node.
OUTPUT — bellman_ford(graph, edges, 'A')
{'A': 0, 'B': 4, 'C': 2, 'D': 5}

Section 06

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.

PassEdgeTestActionDistances after
1C→D (3)∞+3 < ∞skipA0 B∞ C∞ D∞
1B→C (−2)∞−2 < ∞skipA0 B∞ C∞ D∞
1A→B (4)0+4 < ∞ ✓B = 4A0 B4 C∞ D∞
1A→C (5)0+5 < ∞ ✓C = 5A0 B4 C5 D∞
1B→D (10)4+10 < ∞ ✓D = 14A0 B4 C5 D14
2C→D (3)5+3=8 < 14 ✓D = 8A0 B4 C5 D8
2B→C (−2)4−2=2 < 5 ✓C = 2A0 B4 C2 D8
2A→B (4)0+4=4 < 4noA0 B4 C2 D8
2A→C (5)0+5=5 < 2noA0 B4 C2 D8
2B→D (10)4+10=14 < 8noA0 B4 C2 D8
3C→D (3)2+3=5 < 8 ✓D = 5A0 B4 C2 D5
3B→C (−2)4−2=2 < 2noA0 B4 C2 D5
3A→B, A→C, B→Dall ≥noA0 B4 C2 D5
checkall 5 edgesnone improveno negative cycle ✓A0 B4 C2 D5
🎉
Final Shortest Paths from A

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.


Section 07

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.

1 −3 1 X Y Z

X→Y→Z→X sums to 1 + (−3) + 1 = −1 < 0 — a negative cycle.

⚠️
The Detector Pass

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.


Section 08

Bellman-Ford vs Dijkstra

💰 Bellman-Ford
PropertyValue
Negative edgesYes ✓
Detects neg. cycleYes ✓
StrategyRelax all, V−1×
TimeO(V·E)
SpeedSlower
📍 Dijkstra
PropertyValue
Negative edgesNo
Detects neg. cycleNo
StrategyGreedy + heap
TimeO((V+E) log V)
SpeedFaster
🧩
The Trade-Off

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.


Section 09

Time & Space Complexity

CaseWhenComplexity
Time (typical)V−1 passes × E edgesO(V · E)
Time (best, early-exit)No change in a pass → stopO(E)
SpaceOne distance per nodeO(V)
Negative edgesSupportedYes ✓
Negative cycleDetected, not solvedReported
An Easy Optimisation

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.


Section 10

Benefits

Handles Negative Weights
where Dijkstra fails
Refunds, rebates, energy gains — any setting with negative costs is fair game.
🚨
Detects Negative Cycles
diagnostic power
A built-in alarm for loops that lower cost forever — invaluable for spotting arbitrage and instability.
🧬
Simple to Implement
no priority queue
Just two nested loops over the edge list. No heap, no fancy data structures — very easy to get right.
🌐
Distributed-Friendly
distance-vector routing
Its local "tell your neighbours your best distance" nature underpins protocols like RIP.
🎯
Single-Source, All Targets
one run
One execution yields shortest distances from the source to every reachable node.
🔗
Easy Path Recovery
predecessor map
Store a predecessor whenever you relax, and reconstruct the actual route afterwards.

Section 11

When To Use It — and When Not To

Negative Edge Weights
The signature use case. Refunds, discounts, or any cost that can dip below zero — Bellman-Ford handles it correctly.
refunds, rebates
Currency Arbitrage
Model exchange rates as edges; a negative cycle reveals a sequence of trades that yields free profit.
finance, FX trading
Distance-Vector Routing
Network protocols like RIP rely on Bellman-Ford's neighbour-by-neighbour distance updates.
RIP, networking
All Weights Non-Negative
If no edge is negative, Dijkstra is faster and gives the same answer. Don't pay the O(V·E) cost needlessly.
use Dijkstra
Very Large Dense Graphs
O(V·E) becomes expensive when both V and E are huge. Consider Johnson's algorithm or other approaches.
huge V and E
All-Pairs Shortest Paths
Need every pair at once? Floyd-Warshall (or Johnson's for sparse graphs) is the better tool.
use Floyd-Warshall

Section 12

Where Bellman-Ford Sits Among Path Algorithms

AlgorithmSolvesWeightsTimeNeg. cycle?
BFSSingle-sourceUnweightedO(V + E)n/a
DijkstraSingle-sourceNon-negativeO((V+E) log V)No
Bellman-FordSingle-sourceAnyO(V·E)Detects ✓
Floyd-WarshallAll-pairsAnyO(V³)Detects ✓
Johnson'sAll-pairsAnyO(V² log V + VE)Detects ✓
🧩
A Quick Decision Guide

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).


Section 13

Golden Rules

💰 Bellman-Ford — Things to Remember
1
Run exactly V−1 relaxation passes over all edges. That's the guaranteed number to resolve the longest possible shortest path.
2
The V-th pass is the detector. If any edge still relaxes, a negative cycle exists and there is no shortest path — report it.
3
Use it for negative weights; use Dijkstra when all weights are non-negative (it's faster). Same answer, lower cost.
4
It needs directed edges for negative weights. A negative weight on an undirected edge is itself a negative cycle (back-and-forth forever).
5
Add an early-exit flag: if a full pass changes nothing, stop. Distances have already converged.
6
Picture the patient planner re-checking every road each pass, letting good prices ripple one hop further each time. That mental model is the whole algorithm.