Skip to content

Bellman–Ford

Use Bellman–Ford for single-source shortest paths when directed edges may have negative weight and reachable negative cycles must be detected.

Why Dijkstra is invalid

Dijkstra finalizes the next smallest tentative distance. A later negative edge can improve a vertex that appeared final, breaking that proof. Bellman–Ford does not finalize vertices early; it repeatedly relaxes every edge.

State and invariant

distance[v] is the best length found from the source to v.

After pass i, every shortest path using at most i edges has been accounted for. Any simple shortest path contains at most V - 1 edges, so V - 1 passes are enough when no reachable negative cycle exists.

flowchart LR
  accTitle: Bellman-Ford relaxation and negative-cycle flow
  accDescr: Initialize the source, relax every reachable edge for at most V minus one passes, stop at a fixed point, then use one extra pass to detect a reachable negative cycle.
  subgraph S["Build distances"]
    direction TB
    A["Initialize<br/>source 0 · others infinity"] --> B["Relax every reachable edge<br/>for one full pass"]
    B --> C["Repeat until unchanged<br/>or V-1 passes total"]
  end
  subgraph V["Explain why iteration stopped"]
    direction TB
    D{"Fixed point reached early?"} -->|"yes"| E["Distances are final"]
    D -->|"no, pass limit reached"| F["Scan every edge<br/>one extra time"]
  end
  subgraph P["Proof check"]
    direction TB
    G{"Reachable improvement?"} -->|"yes"| H["Negative cycle"]
    G -->|"no"| I["Distances are final"]
  end
  C --> D
  F --> G

Read each full edge scan as one DP layer: it permits shortest paths with one more edge. The final scan is a proof check, not another distance-building pass.

Annotated blueprint

distance[*] = infinity
distance[source] = 0

repeat V - 1 times:
    changed = false
    for each directed edge (u, v, weight):
        if distance[u] is infinity: continue       # edge is unreachable
        if distance[u] + weight < distance[v]:
            distance[v] = distance[u] + weight     # relax one more edge
            changed = true
    if not changed: break                          # fixed point reached early

for each edge (u, v, weight):
    if reachable u can still improve v:
        report a reachable negative cycle

The reachability guard matters: a negative cycle in a disconnected component does not invalidate distances from this source.

Worked signal

Currency exchange can use -log(rate) as an edge weight. A negative cycle then represents a product of exchange rates greater than one—an arbitrage cycle.

Complexity and traps

  • Time: O(VE).
  • Extra space: O(V) without path reconstruction.
  • Use a safe infinity sentinel and guard before addition.
  • Decide whether to return a negative-cycle flag, raise an error, or recover the cycle.
  • Do not confuse “negative edge exists” with “reachable negative cycle exists.”

Reference

Tested template

def bellman_ford(
    vertex_count: int, edges: Sequence[tuple[int, int, int]], source: int
) -> list[float | int]:
    """Return shortest distances, rejecting a reachable negative cycle."""
    _validate_vertex(source, vertex_count, "source")
    for left, right, _weight in edges:
        _validate_vertex(left, vertex_count, "edge endpoint")
        _validate_vertex(right, vertex_count, "edge endpoint")

    distance: list[float | int] = [inf] * vertex_count
    distance[source] = 0
    for _ in range(vertex_count - 1):
        changed = False
        for left, right, weight in edges:
            if distance[left] != inf and distance[left] + weight < distance[right]:
                # Relax only from a vertex the source can reach.
                distance[right] = distance[left] + weight
                changed = True
        if not changed:
            break

    for left, right, weight in edges:
        if distance[left] != inf and distance[left] + weight < distance[right]:
            raise ValueError("reachable negative cycle")
    return distance
inline std::vector<long long> bellman_ford(
    int vertex_count,
    const std::vector<std::tuple<int, int, long long>>& edges, int source) {
  if (vertex_count <= 0) {
    throw std::invalid_argument("vertex_count must be positive");
  }
  validate_vertex(source, static_cast<std::size_t>(vertex_count), "source");
  for (const auto& edge : edges) {
    validate_vertex(std::get<0>(edge), vertex_count, "edge endpoint");
    validate_vertex(std::get<1>(edge), vertex_count, "edge endpoint");
  }
  const long long infinity = std::numeric_limits<long long>::max();
  std::vector<long long> distance(vertex_count, infinity);
  distance[source] = 0;
  for (int pass = 0; pass < vertex_count - 1; ++pass) {
    bool changed = false;
    for (const auto& edge : edges) {
      const int left = std::get<0>(edge);
      const int right = std::get<1>(edge);
      const long long weight = std::get<2>(edge);
      if (distance[left] == infinity) {
        continue;
      }
      if ((weight > 0 && distance[left] > infinity - weight) ||
          (weight < 0 &&
           distance[left] < std::numeric_limits<long long>::min() - weight)) {
        throw std::overflow_error("Bellman-Ford distance overflow");
      }
      const long long candidate = distance[left] + weight;
      if (candidate < distance[right]) {
        // Relax only from a vertex the source can reach.
        distance[right] = candidate;
        changed = true;
      }
    }
    if (!changed) {
      break;
    }
  }
  for (const auto& edge : edges) {
    const int left = std::get<0>(edge);
    const int right = std::get<1>(edge);
    const long long weight = std::get<2>(edge);
    if (distance[left] == infinity) {
      continue;
    }
    if ((weight > 0 && distance[left] > infinity - weight) ||
        (weight < 0 &&
         distance[left] < std::numeric_limits<long long>::min() - weight)) {
      throw std::overflow_error("Bellman-Ford distance overflow");
    }
    if (distance[left] + weight < distance[right]) {
      throw std::invalid_argument("reachable negative cycle");
    }
  }
  return distance;
}
inline std::vector<long long> bellman_ford(
    int vertex_count,
    const std::vector<std::tuple<int, int, long long>>& edges, int source) {
  if (vertex_count <= 0) {
    throw std::invalid_argument("vertex_count must be positive");
  }
  validate_vertex(source, static_cast<std::size_t>(vertex_count), "source");
  for (const auto& edge : edges) {
    validate_vertex(std::get<0>(edge), vertex_count, "edge endpoint");
    validate_vertex(std::get<1>(edge), vertex_count, "edge endpoint");
  }
  const long long infinity = std::numeric_limits<long long>::max();
  std::vector<long long> distance(vertex_count, infinity);
  distance[source] = 0;
  for (int pass = 0; pass < vertex_count - 1; ++pass) {
    bool changed = false;
    for (const auto& edge : edges) {
      const int left = std::get<0>(edge);
      const int right = std::get<1>(edge);
      const long long weight = std::get<2>(edge);
      if (distance[left] == infinity) {
        continue;
      }
      if ((weight > 0 && distance[left] > infinity - weight) ||
          (weight < 0 &&
           distance[left] < std::numeric_limits<long long>::min() - weight)) {
        throw std::overflow_error("Bellman-Ford distance overflow");
      }
      const long long candidate = distance[left] + weight;
      if (candidate < distance[right]) {
        // Relax only from a vertex the source can reach.
        distance[right] = candidate;
        changed = true;
      }
    }
    if (!changed) {
      break;
    }
  }
  for (const auto& edge : edges) {
    const int left = std::get<0>(edge);
    const int right = std::get<1>(edge);
    const long long weight = std::get<2>(edge);
    if (distance[left] == infinity) {
      continue;
    }
    if ((weight > 0 && distance[left] > infinity - weight) ||
        (weight < 0 &&
         distance[left] < std::numeric_limits<long long>::min() - weight)) {
      throw std::overflow_error("Bellman-Ford distance overflow");
    }
    if (distance[left] + weight < distance[right]) {
      throw std::invalid_argument("reachable negative cycle");
    }
  }
  return distance;
}

The tested contract raises only for a negative cycle reachable from source; an unreachable negative cycle cannot affect those distances.