Master Bellman-Ford algorithm completely — why it handles negative weights where Dijkstra fails, the V-1 relaxation rounds, negative cycle detection, SPFA optimization, comparison with Dijkstra, and applications in currency arbitrage and network routing protocols.
Why Bellman-Ford Exists
Dijkstra's algorithm fails with negative edge weights. Bellman-Ford is the solution — it handles negative weights correctly and also detects negative cycles (cycles where the total weight is negative, making shortest path undefined).
| Graph: A | B (weight=4) |
| A | C (weight=2) |
| C | B (weight=-3) ← NEGATIVE! |
| Dijkstra | processes A, extracts B with dist=4 (wrong!) |
| Bellman-Ford: correctly finds A | C→B = 2 + (-3) = -1 |
Algorithm
1. Initialize dist[source] = 0, dist[all others] = infinity
2. Repeat (V-1) times:
For each edge (u, v, w) in the graph:
Relax edge (u, v, w)
3. Check for negative cycles:
For each edge (u, v, w):
If dist[u] + w < dist[v]: negative cycle exists!
Why V-1 iterations? The shortest path in a graph with V vertices has at most V-1 edges. After V-1 relaxations, all shortest paths are found (if no negative cycle exists). If a V-th iteration still finds a shorter path, a negative cycle is reachable.
Complete Implementation
def bellman_ford(edges, n, source):
"""
Bellman-Ford shortest path algorithm.
edges: list of (u, v, weight)
n: number of vertices
source: starting vertex
Returns: (dist, has_negative_cycle)
Time: O(V × E), Space: O(V)
"""
dist = [float('inf')] * n
dist[source] = 0
# V-1 relaxation rounds
for i in range(n - 1):
updated = False
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
updated = True
if not updated:
break # Optimization: if no update, done early
# Check for negative cycles (V-th round)
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
return dist, True # Negative cycle detected!
return dist, False
edges = [
(0, 1, 4),
(0, 2, 2),
(2, 1, -3),
(1, 3, 1),
(2, 3, 5)
]
dist, has_neg_cycle = bellman_ford(edges, 4, 0)
print(dist) # [0, -1, 2, 0]
print(has_neg_cycle) # False
Step-by-Step Trace
| n=4 vertices, edges: (0 | 1,4),(0→2,2),(2→1,-3),(1→3,1),(2→3,5) |
| Initial | dist = [0, ∞, ∞, ∞] |
| (0 | 1, 4): dist[0]+4=4 < ∞ → dist[1]=4 |
| (0 | 2, 2): dist[0]+2=2 < ∞ → dist[2]=2 |
| (2 | 1,-3): dist[2]+(-3)=-1 < 4 → dist[1]=-1 |
| (1 | 3, 1): dist[1]+1=0 < ∞ → dist[3]=0 |
| (2 | 3, 5): dist[2]+5=7 > 0 → no update |
| After round 1 | dist = [0, -1, 2, 0] |
| No edge improves any distance | all converged |
| Final | dist = [0, -1, 2, 0] ✓ |
| No edge improves any distance | no negative cycle ✓ |
Negative Cycle Detection and Reporting
def bellman_ford_detect_negative_cycle(edges, n, source):
"""
Detect negative cycles and report which vertices are affected.
Returns vertices that are in or reachable from a negative cycle.
"""
dist = [float('inf')] * n
parent = [-1] * n
dist[source] = 0
for i in range(n - 1):
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
parent[v] = u
# V-th round: find one vertex in a negative cycle
neg_cycle_vertex = -1
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
neg_cycle_vertex = v
break
if neg_cycle_vertex == -1:
return dist, None # No negative cycle
# Trace back to find the actual cycle
# Walk V more steps to ensure we're IN the cycle
x = neg_cycle_vertex
for _ in range(n):
x = parent[x]
# x is now definitely in the cycle. Trace the cycle.
cycle = []
current = x
while True:
cycle.append(current)
current = parent[current]
if current == x:
break
cycle.append(x)
cycle.reverse()
return dist, cycle
edges = [(0,1,1),(1,2,-3),(2,0,1)] # 0→1→2→0 cycle with weight -1
dist, cycle = bellman_ford_detect_negative_cycle(edges, 3, 0)
print(cycle) # [0, 1, 2, 0] — the negative cycle
SPFA — Shortest Path Faster Algorithm
SPFA is an optimization of Bellman-Ford using a queue. Instead of relaxing ALL edges in each round, only relax edges from vertices whose distance was recently updated.
from collections import deque
def spfa(graph, n, source):
"""
SPFA: queue-based Bellman-Ford optimization.
Average case: O(E), Worst case: O(VE)
Much faster than Bellman-Ford in practice on sparse graphs.
"""
dist = [float('inf')] * n
dist[source] = 0
in_queue = [False] * n
relax_count = [0] * n # Count how many times each vertex relaxed
queue = deque([source])
in_queue[source] = True
while queue:
u = queue.popleft()
in_queue[u] = False
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
relax_count[v] += 1
if relax_count[v] >= n:
return dist, True # Negative cycle!
if not in_queue[v]:
queue.append(v)
in_queue[v] = True
return dist, False
Why SPFA is faster in practice:
- Only vertices with updated distances re-enter the queue
- On sparse graphs with no negative cycles, most vertices enter the queue O(1) times
- Degenerates to O(VE) only on adversarial graphs
Bellman-Ford on Directed Graphs with All-Pairs
def bellman_ford_all_pairs(edges, n):
"""
Run Bellman-Ford from all vertices to get all-pairs shortest paths.
Time: O(V² × E) — much slower than Floyd-Warshall O(V³) for dense graphs.
But better for very sparse graphs.
"""
all_dist = []
for source in range(n):
dist, has_neg = bellman_ford(edges, n, source)
all_dist.append(dist)
return all_dist
Application — Currency Arbitrage Detection
Currencies: USD, EUR, GBP, JPY
Exchange rates form a directed graph.
Negative cycle in log-weight graph = arbitrage opportunity.
Convert exchange rates to log:
weight(u,v) = -log(exchange_rate(u,v))
A negative cycle means the product of rates > 1 → profit!
import math
def detect_arbitrage(rates, currencies):
"""
rates[i][j] = exchange rate from currency i to j.
Returns True if there's an arbitrage opportunity.
"""
n = len(currencies)
edges = []
for i in range(n):
for j in range(n):
if i != j and rates[i][j] > 0:
edges.append((i, j, -math.log(rates[i][j])))
dist = [float('inf')] * n
dist[0] = 0
for _ in range(n - 1):
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# Check for negative cycle
for u, v, w in edges:
if dist[u] + w < dist[v]:
return True # Arbitrage exists!
return False
Bellman-Ford vs Dijkstra
| Feature | Dijkstra | Bellman-Ford |
|---|
| Negative weights | ❌ Fails | ✅ Handles |
| Negative cycles | ❌ Doesn't detect | ✅ Detects |
| Time complexity | O((V+E) log V) | O(V × E) |
| Algorithm type | Greedy | Dynamic programming |
| Best for | Non-negative weights | Any weights, including negative |
| Directed/Undirected | Both | Both |
Summary
Bellman-Ford: relax all edges V-1 times, detect negative cycles in V-th round.
- Time: O(V×E) — slower than Dijkstra but handles negatives
- Negative cycle detection: V-th round finds any remaining improvements
- SPFA: Queue-based optimization — O(E) average case
- Use when: Negative edges present, negative cycle detection needed, distributed settings (Bellman-Ford is the basis of RIP routing protocol)
*Next Lesson: Floyd-Warshall Algorithm*