← All Posts
DSA Series · Graphs · Bellman-Ford

Bellman-Ford Algorithm

Dijkstra’s algorithm is the go-to for shortest paths — until you encounter a negative-weight edge. The moment any edge has a negative cost, Dijkstra’s greedy strategy breaks: a node it marked “final” might later discover a cheaper route through the negative edge. The Bellman-Ford algorithm handles this gracefully. It is slower — O(VE) instead of O(E log V) — but it works correctly with negative weights and can detect negative-weight cycles, something Dijkstra cannot do at all. In this post we will build up the algorithm from intuition, trace it step-by-step on a graph with negative edges, add negative-cycle detection, cover the SPFA optimisation, write clean C++ code, and explore competitive-programming patterns.

When to Use Bellman-Ford

Dijkstra’s algorithm is faster, so why would you ever reach for Bellman-Ford? There are three key scenarios:

Rule of thumb: Use Dijkstra when all weights are non-negative. Use Bellman-Ford when negative weights exist or you need negative-cycle detection. Use SPFA (a Bellman-Ford optimisation) when you want a practical speed-up on sparse graphs.

The Algorithm: Relax All Edges V−1 Times

Bellman-Ford is remarkably simple. It maintains a distance array dist[], initialised to ∞ for all vertices except the source (which is 0). Then it performs V−1 iterations, and in each iteration it relaxes every edge:

function bellmanFord(V, edges, source):
dist = [∞] * V
dist[source] = 0

for i = 1 to V − 1:  // V−1 iterations
for (u, v, w) in edges:  // relax every edge
  if dist[u] + w < dist[v]:
    dist[v] = dist[u] + w

return dist

Why V−1 Iterations Suffice

The key insight: in a graph with V vertices, the shortest path from source to any vertex uses at most V−1 edges (assuming no negative cycles). After iteration k, Bellman-Ford has found the shortest path using at most k edges. After V−1 iterations, all shortest paths are found.

Think of it inductively:

Early termination: If an entire iteration produces no relaxation (no distance improves), the algorithm has converged and we can stop early. In practice this often saves significant work.

Relaxation: The Core Operation

Relaxing an edge (u, v, w) means: if going through u gives v a shorter path, update it. Formally: if dist[u] + w < dist[v], set dist[v] = dist[u] + w. Bellman-Ford simply applies this operation to every edge in every iteration, and the mathematics guarantees convergence.

Step-by-Step Walkthrough

Let’s trace Bellman-Ford on this 6-node directed graph with negative edges. Source is node 0.

Edges (processed in this order each iteration):

EdgeWeight
0 → 16
0 → 24
1 → 3−1
2 → 1−2
2 → 35
2 → 43
3 → 53
4 → 3−2
4 → 56

Initialise dist[]

Set dist[0] = 0 and all others to ∞. Node 0 is the source (green).

6 4 −1 −2 5 3 3 −2 6 0 1 2 3 4 5
Node012345
dist0

Iteration 1: Relax All 9 Edges

Process each edge. When dist[u] is ∞ the edge is skipped (can’t improve anything).

  • 0→1 (w=6): dist[1] = min(∞, 0+6) = 6
  • 0→2 (w=4): dist[2] = min(∞, 0+4) = 4
  • 1→3 (w=−1): dist[3] = min(∞, 6−1) = 5
  • 2→1 (w=−2): dist[1] = min(6, 4−2) = 2 ← improved!
  • 2→3 (w=5): dist[3] = min(5, 4+5) = 5 (no change)
  • 2→4 (w=3): dist[4] = min(∞, 4+3) = 7
  • 3→5 (w=3): dist[5] = min(∞, 5+3) = 8
  • 4→3 (w=−2): dist[3] = min(5, 7−2) = 5 (no change)
  • 4→5 (w=6): dist[5] = min(8, 7+6) = 8 (no change)
6 4 −1 −2 5 3 3 −2 6 0 1 2 3 4 5
Node012345
dist024578
6 distances updated

Iteration 2: Further Refinement

Node 1’s distance improved to 2 in iteration 1. This allows edge 1→3 to produce a shorter path to node 3.

  • 0→1 (w=6): dist[1] = min(2, 0+6) = 2 (no change)
  • 0→2 (w=4): dist[2] = min(4, 0+4) = 4 (no change)
  • 1→3 (w=−1): dist[3] = min(5, 2−1) = 1 ← improved!
  • 2→1 (w=−2): dist[1] = min(2, 4−2) = 2 (no change)
  • 2→3 (w=5): dist[3] = min(1, 4+5) = 1 (no change)
  • 2→4 (w=3): dist[4] = min(7, 4+3) = 7 (no change)
  • 3→5 (w=3): dist[5] = min(8, 1+3) = 4 ← improved!
  • 4→3 (w=−2): dist[3] = min(1, 7−2) = 1 (no change)
  • 4→5 (w=6): dist[5] = min(4, 7+6) = 4 (no change)
6 4 −1 −2 5 3 3 −2 6 0 1 2 3 4 5
Node012345
dist024174
2 distances updated — negative edge 1→3 propagated the improvement

Iteration 3: No Changes — Converged!

We process all 9 edges again. No single relaxation improves any distance. The algorithm detects this and terminates early.

Node012345
dist024174
✅ Converged! Final shortest distances found.

Shortest Paths Summary

The final shortest paths from source 0:

  • 0 → 1: 0 → 2 → 1 (cost 4 − 2 = 2) — via negative edge!
  • 0 → 2: 0 → 2 (cost 4)
  • 0 → 3: 0 → 2 → 1 → 3 (cost 2 − 1 = 1)
  • 0 → 4: 0 → 2 → 4 (cost 4 + 3 = 7)
  • 0 → 5: 0 → 2 → 1 → 3 → 5 (cost 1 + 3 = 4)

Notice how the negative edges 2→1 and 1→3 created shorter paths than the direct positive routes. This is exactly the scenario where Dijkstra would fail.

Detecting Negative Cycles

After V−1 iterations, Bellman-Ford has found all shortest paths — if no negative cycle exists. To check for negative cycles, we run one more iteration (the V-th pass). If any edge can still be relaxed, a negative cycle is reachable from the source.

// After V−1 iterations of Bellman-Ford:
for (u, v, w) in edges:
if dist[u] + w < dist[v]:
return "Negative cycle detected!"

Why Does This Work?

If there are no negative cycles, V−1 iterations are enough — all shortest paths use at most V−1 edges. If a V-th iteration still finds an improvement, it means there exists a path with V edges that is shorter, which is only possible if the path revisits a vertex — meaning it goes around a cycle, and that cycle has negative total weight.

Example: Currency Arbitrage

Imagine four currencies: USD, EUR, GBP, JPY. We model exchange rates as edge weights (using −log(rate) to convert multiplication to addition). A negative cycle means you can trade in a loop and end up with more money than you started with.

1 −3 2 −1 4 USD EUR GBP JPY

The cycle EUR → GBP → JPY → USD → EUR has total weight: (−3) + 2 + (−1) + 1 = −1. This is a negative cycle! Bellman-Ford’s V-th iteration would detect that distances keep decreasing — revealing an arbitrage opportunity.

Finding the cycle itself: When the V-th iteration relaxes an edge (u, v), vertex v is on or reachable from a negative cycle. To find the actual cycle, follow parent pointers from v for V steps (guaranteeing you enter the cycle), then trace until you return to the same vertex.

Interactive Animation

Step through Bellman-Ford on our 6-node graph. Watch the dist[] array update as each iteration relaxes edges. Orange nodes had their distance updated in the current iteration.

Use Step for manual control, Play for auto-advance, or Reset to start over.

6 4 −1 −2 5 3 3 −2 6 0 1 2 3 4 5
Unvisited Updated Finalised

SPFA: Shortest Path Faster Algorithm

The SPFA (Shortest Path Faster Algorithm) is a queue-based optimisation of Bellman-Ford. Instead of blindly relaxing all edges in every iteration, it maintains a queue of “active” vertices — vertices whose distance was recently updated and might propagate improvements.

function SPFA(V, adj, source):
dist = [∞] * V
dist[source] = 0
in_queue = [false] * V
queue = [source]
in_queue[source] = true

while queue not empty:
u = queue.dequeue()
in_queue[u] = false
for (v, w) in adj[u]:
  if dist[u] + w < dist[v]:
    dist[v] = dist[u] + w
    if not in_queue[v]:
      queue.enqueue(v)
      in_queue[v] = true

return dist

When SPFA Helps

When SPFA Fails

SPFA’s worst-case complexity is still O(VE), identical to vanilla Bellman-Ford. Problem setters who know about SPFA can construct “anti-SPFA” graphs (grid-like structures with specific edge weights) that force O(VE) behaviour. In recent competitive programming, many problems are specifically designed to break SPFA, so be cautious.

Negative cycle detection with SPFA: Track how many times each vertex enters the queue. If any vertex enters more than V−1 times, a negative cycle exists. This is the cnt[v] check in the C++ implementation below.

C++ Implementations

Three clean, competitive-programming-ready implementations.

1. Basic Bellman-Ford

#include <bits/stdc++.h>
using namespace std;

struct Edge { int u, v, w; };

vector<long long> bellmanFord(int V, vector<Edge>& edges, int src) {
    const long long INF = 1e18;
    vector<long long> dist(V, INF);
    dist[src] = 0;

    for (int i = 0; i < V - 1; i++) {
        bool updated = false;
        for (auto& [u, v, w] : edges) {
            if (dist[u] < INF && dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                updated = true;
            }
        }
        if (!updated) break;  // early termination
    }
    return dist;
}

int main() {
    int V = 6;
    vector<Edge> edges = {
        {0,1,6}, {0,2,4}, {1,3,-1}, {2,1,-2},
        {2,3,5}, {2,4,3}, {3,5,3}, {4,3,-2}, {4,5,6}
    };

    auto dist = bellmanFord(V, edges, 0);
    for (int i = 0; i < V; i++)
        cout << "dist[" << i << "] = " << dist[i] << "\n";
    return 0;
}

Output:

dist[0] = 0
dist[1] = 2
dist[2] = 4
dist[3] = 1
dist[4] = 7
dist[5] = 4

2. Negative Cycle Detection

#include <bits/stdc++.h>
using namespace std;

struct Edge { int u, v, w; };

// Returns the negative cycle as a vector, or empty if none
vector<int> findNegativeCycle(int V, vector<Edge>& edges) {
    const long long INF = 1e18;
    vector<long long> dist(V, 0);  // init to 0 to detect any cycle
    vector<int> parent(V, -1);
    int last = -1;

    for (int i = 0; i < V; i++) {
        last = -1;
        for (auto& [u, v, w] : edges) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                parent[v] = u;
                last = v;
            }
        }
    }

    if (last == -1) return {};  // no negative cycle

    // Trace back V steps to ensure we're inside the cycle
    int v = last;
    for (int i = 0; i < V; i++) v = parent[v];

    // Reconstruct the cycle
    vector<int> cycle;
    int cur = v;
    do {
        cycle.push_back(cur);
        cur = parent[cur];
    } while (cur != v);
    cycle.push_back(v);
    reverse(cycle.begin(), cycle.end());
    return cycle;
}

int main() {
    int V = 4;
    vector<Edge> edges = {
        {0,1,1}, {1,2,-3}, {2,3,2}, {3,1,-1}
    };
    // Cycle: 1 -> 2 -> 3 -> 1 with weight -3+2+(-1) = -2

    auto cycle = findNegativeCycle(V, edges);
    if (cycle.empty()) {
        cout << "No negative cycle\n";
    } else {
        cout << "Negative cycle: ";
        for (int v : cycle) cout << v << " ";
        cout << "\n";
    }
    return 0;
}

Output:

Negative cycle: 1 2 3 1
Why initialise dist to 0? When detecting negative cycles reachable from any vertex (not just a source), we set all distances to 0. This effectively adds a virtual source node with zero-weight edges to all vertices. Any negative cycle in the graph will be detected.

3. SPFA with Negative Cycle Detection

#include <bits/stdc++.h>
using namespace std;

pair<vector<long long>, bool> spfa(int V,
    vector<vector<pair<int,int>>>& adj, int src)
{
    const long long INF = 1e18;
    vector<long long> dist(V, INF);
    vector<bool> in_queue(V, false);
    vector<int> cnt(V, 0);  // enqueue count
    dist[src] = 0;

    queue<int> q;
    q.push(src);
    in_queue[src] = true;

    while (!q.empty()) {
        int u = q.front(); q.pop();
        in_queue[u] = false;

        for (auto& [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if (!in_queue[v]) {
                    q.push(v);
                    in_queue[v] = true;
                    cnt[v]++;
                    if (cnt[v] >= V)
                        return {dist, true};  // negative cycle
                }
            }
        }
    }
    return {dist, false};
}

int main() {
    int V = 6;
    vector<vector<pair<int,int>>> adj(V);
    adj[0] = {{1,6},{2,4}};
    adj[1] = {{3,-1}};
    adj[2] = {{1,-2},{3,5},{4,3}};
    adj[3] = {{5,3}};
    adj[4] = {{3,-2},{5,6}};

    auto [dist, has_neg] = spfa(V, adj, 0);
    if (has_neg) {
        cout << "Negative cycle detected!\n";
    } else {
        for (int i = 0; i < V; i++)
            cout << "dist[" << i << "] = " << dist[i] << "\n";
    }
    return 0;
}

Output:

dist[0] = 0
dist[1] = 2
dist[2] = 4
dist[3] = 1
dist[4] = 7
dist[5] = 4

Complexity Analysis

Time Complexity

AlgorithmTimeBest For
Bellman-FordO(VE)Negative weights, neg-cycle detection
SPFA (avg)O(E)Sparse graphs in practice
SPFA (worst)O(VE)Same as Bellman-Ford
Dijkstra (binary heap)O(E log V)Non-negative weights
Dijkstra (Fibonacci heap)O(E + V log V)Theoretical optimum, non-negative

Space Complexity

All variants use O(V + E) space: O(E) for the edge list or adjacency list, plus O(V) for the dist[] and parent[] arrays. SPFA adds O(V) for the queue and in_queue[]/cnt[] arrays.

Bellman-Ford vs Dijkstra

Dijkstra’s Algorithm

  • O(E log V) — much faster
  • Greedy: each node finalised once
  • Requires non-negative weights
  • Cannot detect negative cycles
  • Go-to for road networks, grids

Bellman-Ford / SPFA

  • O(VE) — slower but more general
  • Iterative: nodes may be updated many times
  • Handles negative weights
  • Can detect negative cycles
  • Go-to for arbitrage, DP on graphs

Competitive Programming Patterns

Bellman-Ford appears in many disguises in competitive programming. Here are the most common patterns:

1. Arbitrage Detection

Given exchange rates between N currencies, determine if there is a sequence of trades that generates profit from nothing. Model currencies as nodes and −log(rate) as edge weights. A negative cycle in this graph means an arbitrage opportunity.

// Arbitrage detection with Bellman-Ford
bool hasArbitrage(int N, vector<vector<double>>& rate) {
    // rate[i][j] = exchange rate from currency i to j
    vector<double> dist(N, 0);

    for (int iter = 0; iter < N - 1; iter++)
        for (int u = 0; u < N; u++)
            for (int v = 0; v < N; v++)
                if (rate[u][v] > 0) {
                    double w = -log(rate[u][v]);
                    if (dist[u] + w < dist[v])
                        dist[v] = dist[u] + w;
                }

    // Check for negative cycle
    for (int u = 0; u < N; u++)
        for (int v = 0; v < N; v++)
            if (rate[u][v] > 0) {
                double w = -log(rate[u][v]);
                if (dist[u] + w < dist[v])
                    return true;  // arbitrage!
            }
    return false;
}

2. Shortest Path with at Most K Edges

Run Bellman-Ford for exactly K iterations instead of V−1. After iteration K, dist[v] holds the shortest path from source to v using at most K edges. Critical: to avoid using updates from the current iteration, copy the dist array at the start of each iteration.

// Shortest path from src to dst using at most K edges
long long shortestWithKEdges(int V, vector<Edge>& edges,
                              int src, int dst, int K) {
    const long long INF = 1e18;
    vector<long long> dist(V, INF);
    dist[src] = 0;

    for (int i = 0; i < K; i++) {
        vector<long long> prev = dist;  // copy!
        for (auto& [u, v, w] : edges) {
            if (prev[u] < INF && prev[u] + w < dist[v])
                dist[v] = prev[u] + w;
        }
    }
    return dist[dst];
}

This is the key idea behind LeetCode 787: Cheapest Flights Within K Stops.

3. Differential Constraints

A system of difference constraints is a set of inequalities of the form xj − xi ≤ wij. This can be modelled as a shortest-path problem: create edge i → j with weight wij. A feasible solution exists iff the constraint graph has no negative cycle. The shortest distances from a virtual source give a feasible assignment.

// Solve x[j] - x[i] <= w for each constraint (i, j, w)
// Returns feasible assignment or empty if impossible
vector<long long> solveDiffConstraints(int N,
    vector<tuple<int,int,int>>& constraints)
{
    // Add virtual source node N with edges to all nodes (weight 0)
    int V = N + 1, src = N;
    vector<Edge> edges;
    for (auto& [i, j, w] : constraints)
        edges.push_back({i, j, w});
    for (int i = 0; i < N; i++)
        edges.push_back({src, i, 0});

    const long long INF = 1e18;
    vector<long long> dist(V, INF);
    dist[src] = 0;

    for (int iter = 0; iter < V - 1; iter++)
        for (auto& [u, v, w] : edges)
            if (dist[u] < INF && dist[u] + w < dist[v])
                dist[v] = dist[u] + w;

    // Check negative cycle
    for (auto& [u, v, w] : edges)
        if (dist[u] < INF && dist[u] + w < dist[v])
            return {};  // no feasible solution

    dist.pop_back();  // remove virtual source
    return dist;
}

Practice Problems

Test your understanding with these problems, ordered roughly by difficulty:

Network Delay Time (LeetCode 743)

Find the time for a signal to reach all nodes. Direct Bellman-Ford application on a weighted directed graph. Good first problem to implement the basic algorithm.

Cheapest Flights Within K Stops (LeetCode 787)

Shortest path with at most K+1 edges. Run K+1 iterations of Bellman-Ford with the “copy dist before each iteration” trick. Classic K-edge pattern.

High Score (CSES)

Find the maximum-score path from 1 to N. Negate weights and run Bellman-Ford. If N is reachable from a negative cycle, the answer is −1 (infinite score). Tests negative-cycle reachability.

Cycle Finding (CSES)

Find and print a negative cycle if one exists. Direct application of the parent-tracing technique from our negative-cycle detection code.

Arbitrage (SPOJ)

Classic arbitrage detection. Model exchange rates with −log and detect negative cycles. Can also be solved with Floyd-Warshall.

Minimum Path (Codeforces 1473E)

Shortest path where you must add the maximum edge weight and subtract the minimum edge weight. Model with layered graph and SPFA/Bellman-Ford.

Bicycles (Codeforces 1915G)

Shortest path where edge costs depend on a “speed” variable that changes at each node. State-space expansion with Bellman-Ford or modified Dijkstra.

Investigation (CSES)

Find shortest path, count shortest paths, min edges on shortest path, max edges on shortest path. Combine Bellman-Ford or Dijkstra with DP.

Summary

Let’s recap the key takeaways:

  1. Core idea: Bellman-Ford relaxes all edges V−1 times. After iteration k, all shortest paths using ≤ k edges are correct.
  2. Negative weights: Unlike Dijkstra, Bellman-Ford handles negative edge weights correctly because it re-examines edges multiple times.
  3. Negative-cycle detection: Run one extra iteration. If any edge still relaxes, a negative cycle exists. Trace parent pointers to reconstruct it.
  4. SPFA optimisation: Queue-based variant that only relaxes edges from recently-updated vertices. Average case O(E), worst case O(VE). Beware anti-SPFA test cases.
  5. K-edge constraint: Run exactly K iterations (with dist copy per iteration) to find shortest paths using at most K edges.
  6. Differential constraints: Systems of xj − xi ≤ wij map directly to shortest-path problems solvable by Bellman-Ford.
  7. Complexity: O(VE) time, O(V+E) space. Use Dijkstra when weights are non-negative; reach for Bellman-Ford when they aren’t.
Practice tip: The most common Bellman-Ford patterns in competitions are: (1) negative-cycle detection, (2) shortest path with edge count limit, and (3) differential constraints. Master these three and you’ll handle 90% of Bellman-Ford problems.
← Dijkstra’s Algorithm Floyd-Warshall →