On This Page

What Are These Algorithms?

The Problem: Negative Edges

Dijkstra's algorithm is fast โ€” O(E log V) with a priority queue โ€” but it has a blind spot. It uses a greedy strategy (always picking the closest unfinalized node), and that strategy assumes once a node is finalized, its distance won't improve. Negative edge weights break that assumption. A path through a negative edge can retroactively make a finalized node's distance shorter.

Picture a currency exchange. Converting USD โ†’ EUR โ†’ GBP โ†’ USD might actually give you more money than you started with. In graph terms, the exchange rates form edges, and that profitable loop is a negative cycle โ€” a cycle whose total edge weight is negative. If you can loop forever and keep reducing the total cost, then "shortest path" is undefined (it's negative infinity). And Dijkstra has no way to detect this.

Bellman-Ford

Bellman-Ford was developed by Richard Bellman (of dynamic programming fame) and Lester Ford Jr. in the late 1950s. It solves the single-source shortest path problem โ€” finding the shortest distance from one starting node to every other node โ€” and works with negative edge weights. The tradeoff is speed: O(VยทE) instead of Dijkstra's O(E log V). But it can also detect negative cycles, which is genuinely useful.

The core idea is dead simple: relax (try to improve the distance estimate for) every edge, V-1 times. Each pass guarantees at least one more node gets its final shortest distance. If anything still changes on a V-th pass, there's a negative cycle โ€” distances would keep decreasing forever.

Floyd-Warshall

Floyd-Warshall, named after Robert Floyd and Stephen Warshall (who independently worked on similar ideas in the early 1960s), takes a fundamentally different approach. Instead of finding shortest paths from one source, it computes shortest paths between every pair of vertices simultaneously. It runs in O(Vยณ) time using dynamic programming โ€” a technique where you solve a problem by breaking it into overlapping subproblems and caching results.

The DP question it asks at each step: "Can I get from vertex i to vertex j faster by routing through vertex k?" Three nested loops, one elegant recurrence. That's it.

Floyd-Warshall is the right tool when you need the full distance matrix (a Vร—V table of all pairwise shortest distances). Think network routing tables, transitive closure (determining which nodes can reach which other nodes), or checking if every node can reach every other node.

When to Use Each

A Brief History

Bellman published his algorithm in 1958 as part of his work on dynamic programming at RAND Corporation. Ford's version came independently around the same time. The algorithm's simplicity โ€” just a flat loop over all edges, repeated โ€” made it easy to implement on the hardware of the era. Moore independently proposed a similar approach in 1957, which is why you sometimes see the name "Bellman-Ford-Moore."

Floyd-Warshall appeared in 1962. Floyd adapted Warshall's transitive closure algorithm (which determines reachability) to compute shortest paths. The key insight: by considering vertices as intermediate waypoints one at a time, you build up optimal paths incrementally.

Real-World Uses

How They Work

Bellman-Ford: Edge Relaxation

Relaxation is just a fancy word for: "I found a shorter path, let me update." For each edge (u, v) with weight w:

If dist[u] + w < dist[v], set dist[v] = dist[u] + w.

Bellman-Ford does this for every edge, repeating V-1 times. Why V-1? Because the shortest path between any two vertices in a graph with V vertices uses at most V-1 edges (a path that visits every vertex exactly once). After pass k, you're guaranteed to have found all shortest paths that use at most k edges. So after V-1 passes, you've found them all.

Here's the step-by-step:

  1. Initialize: Set distance to source = 0, everything else = โˆž
  2. Relax V-1 times: For each edge (u, v, w), if dist[u] + w < dist[v], update dist[v]
  3. Negative cycle check: One more pass. If anything still updates, you've found a negative cycle โ€” distances would keep decreasing indefinitely
โš ๏ธ Why V-1 passes? Think about a chain: 0 โ†’ 1 โ†’ 2 โ†’ 3 โ†’ 4. If edges happen to be processed in the worst order (4โ†’3 first, then 3โ†’2, etc.), pass 1 only settles vertex 1's distance. Pass 2 settles vertex 2. And so on. V-1 passes guarantees every vertex gets settled regardless of edge processing order. In practice, many graphs converge faster โ€” the early exit optimization catches this.

The SPFA Optimization

Standard Bellman-Ford blindly relaxes every edge every pass, even edges that can't possibly help (because neither endpoint changed). The Shortest Path Faster Algorithm (SPFA) fixes this with a queue: only process vertices whose distances actually changed. In practice, SPFA is much faster. But adversarial inputs can still force O(VยทE) worst case, so it's banned in some competitive programming judges.

Floyd-Warshall: The DP Approach

Floyd-Warshall builds a Vร—V distance matrix, starting with just the direct edges. Then it systematically tries every possible "intermediate" vertex as a waypoint:

dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

For each vertex k, it asks: "Is it shorter to go from i to j through k?" Three nested loops โ€” k, i, j. That's where O(Vยณ) comes from.

  1. Initialize: dist[i][j] = weight(i,j) if edge exists, โˆž otherwise, 0 on the diagonal (distance from any node to itself)
  2. For each intermediate vertex k (0 to V-1):
    • For each pair (i, j): update dist[i][j] if routing through k is shorter
  3. Negative cycle check: If any dist[i][i] < 0, vertex i is part of a negative cycle (a path from i back to i with negative total weight)
๐Ÿ’ก The loop order is sacred The outermost loop MUST be the intermediate vertex k. This isn't a style choice โ€” it's a correctness requirement. The DP recurrence depends on having already considered all intermediates 0..k-1 before processing k. Swapping the loop order gives wrong results. This is the single most common Floyd-Warshall implementation mistake, and interviewers know it.

Interactive Visualization

Watch Bellman-Ford relax edges round by round. The graph includes a negative edge to show how the algorithm handles them. Hit "Run Bellman-Ford" to see distance estimates converge, or "Detect Negative Cycle" to add a negative cycle and watch the algorithm catch it.

Bellman-Ford Relaxation

Common Mistakes

โš ๏ธ Cascading updates in K-limited Bellman-Ford When the problem limits you to K edges (like "cheapest flight with at most K stops"), you MUST copy the distance array before each pass. Without the copy, updates within the same pass cascade: node A updates B, then B updates C โ€” all in one pass, effectively using more edges than allowed. The fix: prev_dist = dist[:] at the start of each pass, and read from prev_dist while writing to dist.
โš ๏ธ Relaxing from unreachable nodes If dist[u] is โˆž and you compute dist[u] + w, in languages with real infinity (Python's float('inf')) this works fine โ€” โˆž + w = โˆž. But in languages with integer types, adding to MAX_VALUE causes overflow, producing a negative number that falsely triggers a relaxation. Always guard: if dist[u] != INF before relaxing.
โš ๏ธ Floyd-Warshall loop order The k loop (intermediate vertex) MUST be outermost. The most natural-looking code puts i outermost, which is wrong. Double-check this every time. Mnemonically: "k-i-j" not "i-j-k."
โš ๏ธ Confusing "negative edge" with "negative cycle" Negative edges are fine โ€” Bellman-Ford handles them correctly. Negative cycles mean shortest paths don't exist (they're -โˆž). Bellman-Ford detects negative cycles but can't give you meaningful shortest paths for nodes reachable from a negative cycle. Make sure you understand which nodes are affected.
โš ๏ธ Using Floyd-Warshall when V is large O(Vยณ) blows up fast. V = 500 means 125 million iterations. V = 1000 means a billion. If V > ~500 and you only need single-source shortest paths, use Bellman-Ford (O(VยทE)) or Dijkstra instead. Floyd-Warshall is for small, dense graphs or when you truly need all pairs.

Operations & Complexity

Algorithm Time Space Negative Edges? Detects Neg. Cycles?
Bellman-Ford O(VยทE) O(V) โœ“ โœ“
SPFA (optimized BF) O(VยทE) worst O(V) โœ“ โœ“
Floyd-Warshall O(Vยณ) O(Vยฒ) โœ“ โœ“
Dijkstra (comparison) O(E log V) O(V) โœ— โœ—

When Bellman-Ford Beats Dijkstra

Despite being slower in the general case, Bellman-Ford wins when:

When Floyd-Warshall Shines

Implementation

Bellman-Ford

Python
def bellman_ford(edges, V, source):
    """Single-source shortest paths with negative edge support.
    
    Args:
        edges: list of (u, v, weight) tuples representing directed edges
        V: number of vertices (labeled 0 to V-1)
        source: starting vertex
    
    Returns:
        (distances, predecessors) โ€” distances[i] is shortest distance from
        source to vertex i, predecessors[i] is the previous vertex on that path.
    
    Raises:
        ValueError if a negative-weight cycle is reachable from source.
    """
    # Initialize: source is 0, everything else is unreachable (infinity)
    dist = [float('inf')] * V
    prev = [-1] * V
    dist[source] = 0

    # Relax every edge V-1 times.
    # Why V-1? A shortest path has at most V-1 edges (visits each vertex once).
    # Each pass settles at least one more vertex's distance.
    for i in range(V - 1):
        updated = False
        for u, v, w in edges:
            # Guard: only relax from reachable vertices
            if dist[u] != float('inf') and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                prev[v] = u
                updated = True
        # Early exit: if nothing changed this pass, we're done early
        if not updated:
            break

    # V-th pass: if we can still relax any edge, a negative cycle exists
    for u, v, w in edges:
        if dist[u] != float('inf') and dist[u] + w < dist[v]:
            raise ValueError("Graph contains a negative-weight cycle")

    return dist, prev

Bellman-Ford with K-Edge Limit

Python
def bellman_ford_k_edges(edges, V, source, K):
    """Shortest paths using at most K edges.
    
    Critical for "cheapest flight with at most K stops" problems.
    The key difference: we copy dist[] before each pass to prevent
    cascading updates within a single pass.
    
    K stops = K+1 edges (you fly K+1 times to make K stops between).
    """
    dist = [float('inf')] * V
    dist[source] = 0

    for _ in range(K):
        # MUST copy before this pass โ€” prevents cascading
        prev_dist = dist[:]
        for u, v, w in edges:
            if prev_dist[u] != float('inf') and prev_dist[u] + w < dist[v]:
                dist[v] = prev_dist[u] + w

    return dist
JavaScript
function bellmanFord(edges, V, source) {
  const dist = new Array(V).fill(Infinity);
  const prev = new Array(V).fill(-1);
  dist[source] = 0;

  // V-1 relaxation passes
  for (let i = 0; i < V - 1; i++) {
    let updated = false;
    for (const [u, v, w] of edges) {
      if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
        dist[v] = dist[u] + w;
        prev[v] = u;
        updated = true;
      }
    }
    if (!updated) break;
  }

  // Negative cycle detection pass
  for (const [u, v, w] of edges) {
    if (dist[u] !== Infinity && dist[u] + w < dist[v]) {
      throw new Error("Negative-weight cycle detected");
    }
  }
  return { dist, prev };
}

Floyd-Warshall

Python
def floyd_warshall(graph):
    """All-pairs shortest paths using dynamic programming.
    
    Args:
        graph: Vร—V adjacency matrix. graph[i][j] = weight of edge iโ†’j.
               Use float('inf') for no edge. Diagonal should be 0.
    
    Returns: Vร—V distance matrix where dist[i][j] = shortest path from i to j.
    
    The DP insight: dist_k[i][j] = shortest path from i to j using only
    vertices 0..k as intermediates.
    dist_k[i][j] = min(dist_{k-1}[i][j], dist_{k-1}[i][k] + dist_{k-1}[k][j])
    We optimize space by updating in-place (safe because reading dist[i][k]
    and dist[k][j] stays correct even after updates with intermediate k).
    """
    V = len(graph)
    dist = [row[:] for row in graph]  # Deep copy โ€” don't mutate input

    # CRITICAL: k must be the outer loop. This is the DP ordering.
    # k = "what intermediates are we allowed to use?"
    for k in range(V):
        for i in range(V):
            for j in range(V):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]

    # Negative cycle check: any negative diagonal means a negative cycle
    for i in range(V):
        if dist[i][i] < 0:
            raise ValueError(f"Negative cycle involving vertex {i}")

    return dist

Floyd-Warshall with Path Reconstruction

Python
def floyd_warshall_with_paths(graph):
    """Floyd-Warshall that also tracks the actual shortest paths.
    
    next_hop[i][j] = the first vertex to go to on the shortest path from i to j.
    To reconstruct path iโ†’j: follow next_hop[i][j], then next_hop[next][j], etc.
    """
    V = len(graph)
    dist = [row[:] for row in graph]
    
    # next_hop[i][j] = first step on shortest path from i to j
    next_hop = [[None] * V for _ in range(V)]
    for i in range(V):
        for j in range(V):
            if i != j and graph[i][j] != float('inf'):
                next_hop[i][j] = j  # Direct edge: go straight to j

    for k in range(V):
        for i in range(V):
            for j in range(V):
                if dist[i][k] + dist[k][j] < dist[i][j]:
                    dist[i][j] = dist[i][k] + dist[k][j]
                    next_hop[i][j] = next_hop[i][k]  # Go toward k first

    def get_path(i, j):
        if next_hop[i][j] is None:
            return []  # No path exists
        path = [i]
        while i != j:
            i = next_hop[i][j]
            path.append(i)
        return path

    return dist, get_path

Real-World Example: Currency Arbitrage Detection

Banks and trading firms use Bellman-Ford to detect arbitrage opportunities โ€” sequences of currency exchanges that yield a profit. If you can convert USD โ†’ EUR โ†’ GBP โ†’ USD and end up with more dollars than you started, that's arbitrage. In graph terms, it's a negative cycle.

The trick: convert multiplication (exchange rates) into addition (which Bellman-Ford understands) by taking the negative logarithm. Multiplying exchange rates becomes summing negative logs. A profitable loop (product > 1) becomes a negative cycle (sum < 0).

Python
import math

def detect_arbitrage(currencies, rates):
    """Detect currency arbitrage using Bellman-Ford.
    
    currencies: list of currency codes, e.g. ["USD", "EUR", "GBP", "JPY"]
    rates: 2D matrix where rates[i][j] = exchange rate from currency i to j
           (1 unit of i buys rates[i][j] units of j)
    
    Returns: (has_arbitrage, cycle) where cycle is the profitable loop if found.
    
    Math: We want to detect if any cycle has product of rates > 1.
    Taking -log: product > 1 โ†’ sum of -log(rates) < 0 โ†’ negative cycle.
    """
    V = len(currencies)
    
    # Build edge list with -log weights
    edges = []
    for i in range(V):
        for j in range(V):
            if i != j and rates[i][j] > 0:
                # -log(rate): a profitable exchange becomes a negative edge
                weight = -math.log(rates[i][j])
                edges.append((i, j, weight))
    
    # Run Bellman-Ford from vertex 0 (choice doesn't matter for cycle detection)
    dist = [float('inf')] * V
    prev = [-1] * V
    dist[0] = 0
    
    for _ in range(V - 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
    
    # V-th pass: find a vertex involved in a negative cycle
    for u, v, w in edges:
        if dist[u] != float('inf') and dist[u] + w < dist[v]:
            # Found a negative cycle โ€” trace it back
            cycle = []
            node = v
            for _ in range(V):  # Walk back V steps to ensure we're in the cycle
                node = prev[node]
            
            # Now trace the cycle
            start = node
            cycle.append(currencies[start])
            node = prev[start]
            while node != start:
                cycle.append(currencies[node])
                node = prev[node]
            cycle.append(currencies[start])
            cycle.reverse()
            
            return True, cycle
    
    return False, []

# Example with real-ish exchange rates
currencies = ["USD", "EUR", "GBP", "JPY", "CHF"]
rates = [
    #  USD    EUR    GBP    JPY      CHF
    [1.0,   0.92,  0.79,  149.50,  0.88],   # USD
    [1.09,  1.0,   0.86,  163.20,  0.96],   # EUR
    [1.27,  1.17,  1.0,   189.50,  1.12],   # GBP
    [0.0067,0.0061,0.0053,1.0,     0.0059], # JPY
    [1.14,  1.05,  0.90,  170.10,  1.0],    # CHF
]

found, cycle = detect_arbitrage(currencies, rates)
if found:
    print(f"Arbitrage found: {' โ†’ '.join(cycle)}")
else:
    print("No arbitrage opportunity")

In practice, real arbitrage opportunities are tiny (fractions of a cent) and disappear in milliseconds. High-frequency trading firms run this detection continuously, competing to exploit opportunities before anyone else. The algorithm itself is simple โ€” the engineering challenge is speed.

Interview Patterns

๐Ÿ’ก "At most K stops" = Bellman-Ford with K iterations When a problem says "find cheapest flight with at most K stops," that's Bellman-Ford with exactly K+1 relaxation passes instead of V-1. Each pass represents one more hop. CRITICAL: copy the distance array before each pass to prevent cascading. This is LeetCode 787 (Cheapest Flights Within K Stops) โ€” one of the most asked graph problems at Google and Meta.
๐Ÿ’ก Negative cycle detection is an arbitrage detector If you see a problem about currency exchange, rate conversion, or "maximize a product along a path," think Bellman-Ford. Take the log of the exchange rates to convert multiplication into addition, negate them, then check for negative cycles. A negative cycle = an arbitrage opportunity. This transformation (log + negate) is a classic technique worth memorizing.
๐Ÿ’ก Floyd-Warshall is often hiding behind "all pairs" If V โ‰ค 200-400 and the problem asks about paths between every pair of nodes, Floyd-Warshall is the clean answer. Three nested for-loops, no priority queue, no adjacency list โ€” just a matrix. Problems like "city connections" or "minimum cost to reach every other node" or "find the city with fewest reachable neighbors" are Floyd-Warshall in disguise.
๐Ÿ’ก Transitive Closure = Floyd-Warshall with Boolean OR Replace min(dist[i][j], dist[i][k] + dist[k][j]) with reachable[i][j] = reachable[i][j] OR (reachable[i][k] AND reachable[k][j]). Now you have a matrix telling you whether any path exists between each pair โ€” that's the transitive closure. Useful for "can node i eventually reach node j?" problems.
๐Ÿ’ก "Shortest path on a DAG" = One pass of Bellman-Ford in topological order If the graph is a DAG (no cycles), you can find shortest paths in O(V+E) โ€” faster than both Dijkstra and Bellman-Ford. Process vertices in topological order and relax each vertex's outgoing edges once. This works because in a topological ordering, by the time you process a vertex, all paths leading to it have already been considered.

Common Patterns

Practice Problems

Start with the tagged difficulty and work your way up.