⚡ Bellman-Ford Fundamentals
🎯 What is the Bellman-Ford Algorithm?
Bellman-Ford is a shortest path algorithm that computes shortest paths from a single source vertex to all other vertices in a weighted graph. Unlike Dijkstra, it works with graphs that have negative edge weights and can detect negative cycles. It was developed by Richard Bellman and Lester Ford Jr. in the 1950s.
Deep Explanation
Core Insight: Bellman-Ford relaxes all edges V-1 times. After i iterations, the algorithm has found the shortest paths using at most i edges. Since a shortest path can have at most V-1 edges (no cycles), V-1 iterations are sufficient. An extra iteration detects negative cycles.
Key Intuition: The algorithm systematically overestimates distances, then repeatedly improves them by "relaxing" edges — checking if going through another vertex provides a shorter path.
def bellman_ford(vertices, edges, source):
# edges: list of (u, v, weight)
# vertices: number of nodes (0-indexed)
dist = [float('inf')] * vertices
dist[source] = 0
# Relax all edges V-1 times
for _ in range(vertices - 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 # Early termination if no updates
# Check for negative cycles
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
return None # Negative cycle detected
return dist
# Example usage
edges = [
(0, 1, 4), (0, 2, 5), (1, 2, -2),
(1, 3, 6), (2, 3, 1), (3, 4, 3)
]
print(bellman_ford(5, edges, 0)) # [0, 4, 2, 3, 6]📐 Algorithm Walkthrough
🎯 Step-by-Step Example
Graph: 0→1 (4), 0→2 (5), 1→2 (-2), 1→3 (6), 2→3 (1), 3→4 (3). Source = 0.
- Initialization: dist = [0, ∞, ∞, ∞, ∞]
- Iteration 1: Relax 0→1: dist[1]=4, 0→2: dist[2]=5, 1→2: 4-2=2 <5 → dist[2]=2, 1→3: 4+6=10, 2→3: 2+1=3 <10 → dist[3]=3, 3→4: 3+3=6 → dist[4]=6
- Iteration 2: 0→1 (no change), 0→2 (no change), 1→2: 4-2=2 (no change), 1→3: 4+6=10 >3, 2→3: 2+1=3 (no change), 3→4: 3+3=6 (no change). Done.
Why V-1 Iterations?
In a graph without negative cycles, the longest shortest path can have at most V-1 edges. After k iterations, we know shortest paths using ≤ k edges. After V-1 iterations, we have the global shortest paths.
⏱️ Time & Space Complexity
🎯 Complexity Analysis
- Time Complexity: O(V × E) — V-1 iterations × E edges per iteration
- Space Complexity: O(V) for distance array + O(E) to store edges
- Early Termination: Can stop early if no distances update in an iteration (shown in code)
- Worst-case: O(V×E) — happens when graph is dense or requires all iterations
Complexity Comparison
For dense graphs where E ≈ V², Bellman-Ford becomes O(V³) — slower than Floyd-Warshall. For sparse graphs (E ≈ V), it's O(V²), making it practical for many real-world applications.
⚠️ Handling Negative Weights
🎯 Why Bellman-Ford Works with Negatives
Unlike Dijkstra's greedy approach (which fails with negative edges), Bellman-Ford uses dynamic programming. It systematically considers all possible paths and can handle negative edges because it doesn't assume that once a node is "finalized" its distance is optimal.
Important Limitation: The graph must have no negative cycles reachable from the source. If a negative cycle exists, shortest paths are undefined (can be arbitrarily short).
# Example where Dijkstra fails but Bellman-Ford works
edges_negative = [
(0, 1, 4), (0, 2, 5), (1, 2, -3), # Negative edge from 1 to 2
(2, 3, 1)
]
# Dijkstra would set dist[2]=5 (via 0→2), but correct is 4+(-3)=1
# Bellman-Ford finds correct: [0, 4, 1, 2]🔄 Negative Cycle Detection
🎯 Identifying Negative Cycles
After V-1 relaxation iterations, run one additional iteration. If any distance can still be reduced, a negative cycle exists in the graph. This is one of Bellman-Ford's most powerful features — it's the only single-source shortest path algorithm that reliably detects negative cycles.
def detect_negative_cycle(vertices, edges, source):
dist = [float('inf')] * vertices
dist[source] = 0
# Relax V-1 times
for _ in range(vertices - 1):
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# Check for negative cycle
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
return True # Negative cycle found
return False
# Graph with negative cycle: 0→1 (2), 1→2 (3), 2→0 (-10)
# This returns True💻 Multiple Implementations
# 1. With path reconstruction
def bellman_ford_with_path(vertices, edges, source):
dist = [float('inf')] * vertices
prev = [-1] * vertices
dist[source] = 0
for _ in range(vertices - 1):
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
prev[v] = u
# Check for negative cycles
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
return None, None # Negative cycle
# Reconstruct path to target
def get_path(target):
path = []
curr = target
while curr != -1:
path.append(curr)
curr = prev[curr]
return path[::-1]
return dist, get_path
# 2. Using adjacency list for better memory
def bellman_ford_adjacency(vertices, graph, source):
# graph: adjacency list with (neighbor, weight)
dist = [float('inf')] * vertices
dist[source] = 0
for _ in range(vertices - 1):
for u in range(vertices):
if dist[u] != float('inf'):
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
return dist📊 Step-by-Step Visualization
🎯 Graph with Negative Edge
Nodes: A (source), B, C, D. Edges: A→B (3), A→C (5), B→C (1), B→D (7), C→D (-2).
- Initial: dist = [A:0, B:∞, C:∞, D:∞]
- Iteration 1: A→B:3, A→C:5, B→C:3+1=4 (update C:4), B→D:3+7=10, C→D:4-2=2 (update D:2)
- Iteration 2: Check all edges again. A→C:5 unchanged, C→D:4-2=2 unchanged. No further updates.
- Result: distances: A:0, B:3, C:4, D:2
✅ Correctness Proof & Relaxation
🎯 The Relaxation Principle
Relaxation Invariant: After i iterations, dist[v] is the length of the shortest path from source to v using at most i edges. By induction, after V-1 iterations, we have the shortest path using any number of edges (since any shortest path has at most V-1 edges without negative cycles). The algorithm is correct due to the triangle inequality property.
📈 Comparison with Dijkstra & SPFA
- vs Dijkstra: Dijkstra is O((V+E)log V) but fails with negative weights. Bellman-Ford handles negative weights but is slower.
- vs SPFA (Shortest Path Faster Algorithm): SPFA is a queue-optimized Bellman-Ford that averages O(E) but can degrade to O(VE). Bellman-Ford has guaranteed O(VE).
- vs Floyd-Warshall: Bellman-Ford is single-source; Floyd-Warshall is all-pairs (O(V³)).
- When to use Bellman-Ford: Negative edges exist, negative cycle detection needed, moderate-sized graphs.
⚡ Optimizations & SPFA
🎯 SPFA - Shortest Path Faster Algorithm
SPFA uses a queue to track which vertices' distances improved, only relaxing edges from those vertices. In practice, it's often faster than Bellman-Ford, though worst-case remains O(VE).
from collections import deque
def spfa(vertices, edges, source):
graph = [[] for _ in range(vertices)]
for u, v, w in edges:
graph[u].append((v, w))
dist = [float('inf')] * vertices
in_queue = [False] * vertices
count = [0] * vertices
dist[source] = 0
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
if not in_queue[v]:
queue.append(v)
in_queue[v] = True
count[v] += 1
if count[v] >= vertices: # Negative cycle
return None
return dist🌍 Real-World Applications
- Network Routing Protocols: RIP (Routing Information Protocol) uses distance-vector routing based on Bellman-Ford.
- Arbitrage Detection: Currency exchange markets - detecting negative cycles in log-weighted graphs indicates arbitrage opportunities.
- Difference Constraints: Solving systems of inequalities (x₁ - x₂ ≤ c).
- Minimum Mean Weight Cycle: Used in financial and scheduling problems.
- GPS/Telecommunications: When negative "costs" represent benefits or discounts.
🌐 Distributed Bellman-Ford
🎯 Distance-Vector Routing
In distributed networks, each node only knows its neighbors. The distributed Bellman-Ford algorithm (used in RIP) allows nodes to compute shortest paths by exchanging distance vectors. This is fully decentralized and robust to topology changes.
🔄 Variants & Extensions
- Moore-Bellman-Ford: Early termination when no updates occur.
- Yen's Optimization: Process edges in a random order to potentially find solutions faster.
- Dynamic Bellman-Ford: Updates after edge weight changes.
- Gauss-Seidel Bellman-Ford: Uses updated values immediately within the same iteration.
💪 Practice & Problem Patterns
🎯 Classic Bellman-Ford Problems
- Cheapest Flights Within K Stops (LeetCode 787): Modified Bellman-Ford with hop constraint.
- Network Delay Time (LeetCode 743): Compare Dijkstra vs Bellman-Ford.
- Find if there is a negative cycle (GFG): Standard detection problem.
- Arbitrage (LeetCode 514): Currency exchange → log conversion → negative cycle detection.
- Minimum Cost to Reach Destination with at most K edges: K-constrained shortest path.
# LeetCode 787 - Cheapest Flights Within K Stops
def findCheapestPrice(n, flights, src, dst, k):
dist = [float('inf')] * n
dist[src] = 0
for i in range(k + 1):
temp = dist.copy()
for u, v, w in flights:
if dist[u] != float('inf') and dist[u] + w < temp[v]:
temp[v] = dist[u] + w
dist = temp
return dist[dst] if dist[dst] != float('inf') else -1