Bellman-Ford is the shortest-path algorithm you reach for when Dijkstra fails — specifically when graphs contain negative-weight edges or you need to detect negative cycles. It appears in finance (arbitrage detection), routing protocols (RIP), and interviews at companies asking about weighted graph problems.
When to Use Bellman-Ford vs Dijkstra
| Criterion | Dijkstra | Bellman-Ford |
|---|---|---|
| Negative edge weights | Fails (greedy assumption broken) | Handles correctly |
| Negative cycles | Cannot detect | Detects explicitly |
| Time complexity | O((V + E) log V) with min-heap | O(V * E) |
| When to use | Non-negative weights, performance-critical | Negative weights or cycle detection needed |
How Bellman-Ford Works
Key insight: any shortest path in a graph with V vertices has at most V-1 edges (if it had V or more, it would visit a vertex twice — a cycle). So we relax all edges V-1 times. After V-1 iterations, if we can still relax any edge, a negative cycle exists.
def bellman_ford(n: int, edges: list[tuple], src: int) -> tuple[list[float], bool]:
"""
Bellman-Ford shortest path from src to all vertices.
edges: list of (u, v, weight)
Returns: (dist[], has_negative_cycle)
Time: O(V * E), Space: O(V)
"""
INF = float('inf')
dist = [INF] * n
dist[src] = 0
# Relax all edges V-1 times
for _ in range(n - 1):
updated = False
for u, v, w in edges:
if dist[u] != INF and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
updated = True
if not updated:
break # Early termination if no updates
# V-th relaxation: if anything updates, negative cycle exists
has_negative_cycle = False
for u, v, w in edges:
if dist[u] != INF and dist[u] + w < dist[v]:
has_negative_cycle = True
break
return dist, has_negative_cycle
# Example: graph with 5 vertices
n = 5
edges = [
(0, 1, -1),
(0, 2, 4),
(1, 2, 3),
(1, 3, 2),
(1, 4, 2),
(3, 2, 5),
(3, 1, 1),
(4, 3, -3),
]
dist, cycle = bellman_ford(n, edges, src=0)
print(f"Distances from 0: {dist}") # [0, -1, 2, -2, 1]
print(f"Negative cycle: {cycle}") # False
Reconstructing the Path
def bellman_ford_with_path(n: int, edges: list[tuple], src: int, dst: int):
"""Returns (distance, path) or (inf, []) if no path or negative cycle."""
INF = float('inf')
dist = [INF] * n
prev = [-1] * n
dist[src] = 0
for _ in range(n - 1):
for u, v, w in edges:
if dist[u] != INF and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
prev[v] = u
# Check for negative cycles on the path to dst
for u, v, w in edges:
if dist[u] != INF and dist[u] + w < dist[v]:
return INF, [] # Negative cycle
# Reconstruct path
path = []
node = dst
while node != -1:
path.append(node)
node = prev[node]
path.reverse()
if path[0] != src:
return INF, [] # No path exists
return dist[dst], path
Application: Currency Arbitrage Detection
Convert currency exchange rates to log space — a negative cycle in log-transformed weights means an arbitrage opportunity exists.
import math
def find_arbitrage(currencies: list[str], rates: list[list[float]]) -> bool:
"""
Detect currency arbitrage: a sequence of conversions that returns
more money than you started with.
Transform: weight = -log(rate)
Arbitrage exists ↔ negative cycle in transformed graph
"""
n = len(currencies)
edges = []
for i in range(n):
for j in range(n):
if i != j and rates[i][j] > 0:
# Negate log because Bellman-Ford finds minimum
edges.append((i, j, -math.log(rates[i][j])))
# Run Bellman-Ford from each source to catch all components
for src in range(n):
_, has_cycle = bellman_ford(n, edges, src)
if has_cycle:
return True # Arbitrage opportunity detected
return False
SPFA (Shortest Path Faster Algorithm)
Queue-optimized Bellman-Ford — only re-relaxes nodes whose distance was just updated:
from collections import deque
def spfa(n: int, graph: list[list[tuple]], src: int) -> list[float]:
"""
SPFA: Bellman-Ford with queue optimization.
Average case much faster; worst case still O(V * E).
"""
INF = float('inf')
dist = [INF] * n
dist[src] = 0
in_queue = [False] * n
in_queue[src] = True
queue = deque([src])
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
if not in_queue[v]:
in_queue[v] = True
queue.append(v)
return dist
LeetCode Problems
- LeetCode 743: Network Delay Time (Dijkstra or Bellman-Ford)
- LeetCode 787: Cheapest Flights Within K Stops (Bellman-Ford with K limit)
- LeetCode 1334: Find the City With Smallest Number of Neighbors at Threshold (Floyd-Warshall)
K-Stop Constraint (LeetCode 787)
def find_cheapest_price(n: int, flights: list[list[int]],
src: int, dst: int, k: int) -> int:
"""
Cheapest flight from src to dst with at most k stops (k+1 edges).
Bellman-Ford limited to k+1 relaxation rounds.
"""
INF = float('inf')
dist = [INF] * n
dist[src] = 0
for _ in range(k + 1): # At most k+1 edges = k stops
temp = dist[:] # Must copy to prevent using same-round updates
for u, v, price in flights:
if dist[u] != INF and dist[u] + price < temp[v]:
temp[v] = dist[u] + price
dist = temp
return dist[dst] if dist[dst] != INF else -1
Related Graph Topics
- BFS vs DFS — Bellman-Ford is a relaxation algorithm, not a traversal; but understanding graph traversal is prerequisite for shortest path algorithms
- Minimum Spanning Tree — MST minimizes total edge weight connecting all nodes; Bellman-Ford finds shortest path from one source; related but different objectives