Bellman-Ford Algorithm and Distance Vector Routing (BGL Book Chapter 5.3)

Based on “The Boost Graph Library” by Jeremy Siek, Lie-Quan Lee, and Andrew Lumsdaine

Overview

The routing problem can be modeled as a graph by associating a vertex with each router and an edge for each direct connection between two routers. Information such as delay and bandwidth are attached to each edge. The routing problem is now transformed into a shortest-paths graph problem.

The ultimate goal of a routing process is to deliver packets to their destinations as quickly as possible. There are a number of factors that determine how long a packet takes to arrive (e.g., the number of hops along the path, transmission delay within a router, transmission delay between routers, network bandwidth). The routing protocol must choose the best paths between routers; this information is stored in a routing table.

Distance Vector Routing

Some of the first Internet routing protocols, such as the Routing Information Protocol (RIP), used a distance-vector protocol. The basic idea behind RIP is for each router to maintain an estimate of distance to all other routers and to periodically compare notes with its neighbors.

If a router learns of a shorter path to some destination from one of its neighbors, it will update its distance record to that destination and change its routing table to send packets to that destination via that neighbor. After enough time, the estimated distances maintained in this distributed fashion by multiple routers are guaranteed to converge to the true distance.

The Bellman-Ford Algorithm

RIP is a distributed form of the Bellman-Ford shortest paths algorithm. The principle step in the Bellman-Ford algorithm, called edge relaxation, corresponds to the notion of “comparing notes with your neighbor.”

The relaxation operation applied to edge (u, v) performs the following update:

d[v] = min(w(u, v) + d[u], d[v])

The Bellman-Ford algorithm loops through all of the edges in a graph, applying the relaxation operation to each. The algorithm repeats this loop |V| times, after which it is guaranteed that the distances to each vertex have been reduced to the minimum possible (unless there is a negative cycle in the graph).

Negative Cycle Detection

If there is a negative cycle, then there will be edges in the graph that were not properly minimized. To verify that all edges are minimized, the algorithm loops over all of the edges in the graph a final time, returning true if they are minimized, and returning false otherwise.

Comparison with Dijkstra

Feature

Bellman-Ford

Dijkstra

Time complexity

O(V * E)

O((V+E) log V)

Negative weights

Yes

No

Negative cycle detect

Yes

No

Distributed version

Yes (RIP)

No

NWGraph Implementation

NWGraph provides a bellman_ford function that implements the Bellman-Ford algorithm.

Listing 5 Complete source code
  1/**
  2 * @file ch5_bellman_ford.cpp
  3 *
  4 * @brief Bellman-Ford Algorithm and Distance Vector Routing (BGL Book Chapter 5.3)
  5 *
  6 * This example demonstrates the Bellman-Ford single-source shortest-paths algorithm
  7 * applied to a router network. The Bellman-Ford algorithm can handle graphs with
  8 * negative edge weights (unlike Dijkstra) and can detect negative cycles.
  9 *
 10 * The algorithm is the basis for distance-vector routing protocols like RIP
 11 * (Routing Information Protocol), where routers periodically exchange distance
 12 * estimates with neighbors.
 13 *
 14 * Key differences from Dijkstra:
 15 * - Can handle negative edge weights
 16 * - Slower: O(V*E) vs O((V+E)log V)
 17 * - Can detect negative cycles
 18 *
 19 * @copyright SPDX-FileCopyrightText: 2022 Battelle Memorial Institute
 20 * @copyright SPDX-FileCopyrightText: 2022 University of Washington
 21 *
 22 * SPDX-License-Identifier: BSD-3-Clause
 23 *
 24 * @authors
 25 *   Andrew Lumsdaine
 26 *
 27 */
 28
 29#include <iostream>
 30#include <limits>
 31#include <vector>
 32
 33#include "nwgraph/adjacency.hpp"
 34#include "nwgraph/edge_list.hpp"
 35
 36using namespace nw::graph;
 37
 38/**
 39 * @brief Bellman-Ford shortest paths algorithm
 40 *
 41 * Computes shortest paths from a source vertex to all other vertices.
 42 * Returns false if a negative cycle is detected, true otherwise.
 43 *
 44 * The algorithm works by repeatedly relaxing all edges |V|-1 times.
 45 * After this, all shortest paths are found (if no negative cycles exist).
 46 * A final pass checks for negative cycles.
 47 */
 48template <typename EdgeList, typename Weight>
 49auto bellman_ford(const EdgeList& edges, size_t num_vertices, size_t source, Weight weight_fn)
 50    -> std::tuple<bool, std::vector<double>, std::vector<size_t>> {
 51
 52  std::vector<double> distance(num_vertices, std::numeric_limits<double>::max());
 53  std::vector<size_t> predecessor(num_vertices);
 54
 55  // Initialize predecessor to self (no predecessor)
 56  for (size_t i = 0; i < num_vertices; ++i) {
 57    predecessor[i] = i;
 58  }
 59
 60  distance[source] = 0;
 61
 62  // Relax all edges |V|-1 times
 63  for (size_t i = 0; i < num_vertices - 1; ++i) {
 64    bool changed = false;
 65    for (auto&& edge : edges) {
 66      auto u = std::get<0>(edge);
 67      auto v = std::get<1>(edge);
 68      auto w = weight_fn(edge);
 69
 70      // Relaxation step
 71      if (distance[u] != std::numeric_limits<double>::max() && distance[u] + w < distance[v]) {
 72        distance[v]    = distance[u] + w;
 73        predecessor[v] = u;
 74        changed        = true;
 75      }
 76    }
 77    // Early termination if no changes
 78    if (!changed) break;
 79  }
 80
 81  // Check for negative cycles
 82  for (auto&& edge : edges) {
 83    auto u = std::get<0>(edge);
 84    auto v = std::get<1>(edge);
 85    auto w = weight_fn(edge);
 86
 87    if (distance[u] != std::numeric_limits<double>::max() && distance[u] + w < distance[v]) {
 88      // Negative cycle detected
 89      return {false, distance, predecessor};
 90    }
 91  }
 92
 93  return {true, distance, predecessor};
 94}
 95
 96int main() {
 97  std::cout << "=== Bellman-Ford Algorithm and Distance Vector Routing ===" << std::endl;
 98  std::cout << "Based on BGL Book Chapter 5.3" << std::endl << std::endl;
 99
100  // Router network from the BGL book example
101  // Vertices represent routers: A=0, B=1, C=2, D=3, E=4, F=5, G=6, H=7
102  // Edge weights represent transmission delays
103  const char* router_names = "ABCDEFGH";
104  const size_t num_routers = 8;
105
106  std::cout << "Router network with transmission delays:" << std::endl;
107  std::cout << "  A connects to B (5.0) and C (1.0)" << std::endl;
108  std::cout << "  B connects to D (1.3) and E (3.0)" << std::endl;
109  std::cout << "  C connects to E (10.0) and F (2.0)" << std::endl;
110  std::cout << "  D connects to E (0.4) and H (6.3)" << std::endl;
111  std::cout << "  E connects to H (1.3)" << std::endl;
112  std::cout << "  F connects to G (1.2)" << std::endl;
113  std::cout << "  G connects to H (0.5)" << std::endl;
114  std::cout << std::endl;
115
116  // Create edge list with transmission delays
117  // Using directed edges as in the BGL book example
118  edge_list<directedness::directed, double> edges(num_routers);
119  edges.open_for_push_back();
120
121  // Edges from the BGL book Figure 5.2
122  edges.push_back(0, 1, 5.0);   // A -> B
123  edges.push_back(0, 2, 1.0);   // A -> C
124  edges.push_back(1, 3, 1.3);   // B -> D
125  edges.push_back(1, 4, 3.0);   // B -> E
126  edges.push_back(2, 4, 10.0);  // C -> E
127  edges.push_back(2, 5, 2.0);   // C -> F
128  edges.push_back(3, 7, 6.3);   // D -> H
129  edges.push_back(3, 4, 0.4);   // D -> E
130  edges.push_back(4, 7, 1.3);   // E -> H
131  edges.push_back(5, 6, 1.2);   // F -> G
132  edges.push_back(6, 7, 0.5);   // G -> H
133
134  edges.close_for_push_back();
135
136  // Run Bellman-Ford from router A (vertex 0)
137  size_t source = 0;  // Router A
138  std::cout << "Computing shortest paths from router " << router_names[source] << "..." << std::endl;
139  std::cout << std::endl;
140
141  auto weight_fn = [](auto&& edge) { return std::get<2>(edge); };
142  auto [success, distance, predecessor] = bellman_ford(edges, num_routers, source, weight_fn);
143
144  if (success) {
145    std::cout << "Shortest path distances and predecessors:" << std::endl;
146    std::cout << std::string(40, '-') << std::endl;
147
148    for (size_t i = 0; i < num_routers; ++i) {
149      std::cout << "  " << router_names[i] << ": ";
150      if (distance[i] == std::numeric_limits<double>::max()) {
151        std::cout << "unreachable";
152      } else {
153        std::cout << distance[i] << "  (via " << router_names[predecessor[i]] << ")";
154      }
155      std::cout << std::endl;
156    }
157
158    // Trace path to H
159    std::cout << std::endl;
160    std::cout << "Shortest path from A to H:" << std::endl;
161    std::cout << "  ";
162
163    std::vector<size_t> path;
164    size_t current = 7;  // H
165    while (current != source) {
166      path.push_back(current);
167      current = predecessor[current];
168    }
169    path.push_back(source);
170
171    for (auto it = path.rbegin(); it != path.rend(); ++it) {
172      std::cout << router_names[*it];
173      if (it + 1 != path.rend()) std::cout << " -> ";
174    }
175    std::cout << std::endl;
176    std::cout << "  Total delay: " << distance[7] << std::endl;
177
178  } else {
179    std::cout << "Negative cycle detected in the network!" << std::endl;
180  }
181
182  // Example with a negative cycle
183  std::cout << std::endl;
184  std::cout << "=== Example with Negative Cycle ===" << std::endl;
185
186  edge_list<directedness::directed, double> edges2(4);
187  edges2.open_for_push_back();
188  edges2.push_back(0, 1, 1.0);
189  edges2.push_back(1, 2, -1.0);
190  edges2.push_back(2, 3, -1.0);
191  edges2.push_back(3, 1, -1.0);  // Creates negative cycle: 1 -> 2 -> 3 -> 1 (cost = -3)
192  edges2.close_for_push_back();
193
194  auto [success2, distance2, predecessor2] = bellman_ford(edges2, 4, 0, weight_fn);
195
196  if (success2) {
197    std::cout << "No negative cycle found." << std::endl;
198  } else {
199    std::cout << "Negative cycle detected! (as expected)" << std::endl;
200    std::cout << "The cycle 1 -> 2 -> 3 -> 1 has total weight -3" << std::endl;
201  }
202
203  std::cout << std::endl;
204  std::cout << "Note: Bellman-Ford is O(V*E), slower than Dijkstra's O((V+E)log V)," << std::endl;
205  std::cout << "but can handle negative weights and detect negative cycles." << std::endl;
206
207  return 0;
208}

Building and Running the Example

First, configure and build the example:

# From the NWGraph root directory
mkdir -p build && cd build
cmake .. -DNWGRAPH_BUILD_EXAMPLES=ON
make ch5_bellman_ford

Then run the example:

./examples/bgl-book/ch5_bellman_ford

The program uses a sample router network and computes shortest paths using the Bellman-Ford algorithm, demonstrating how distance-vector routing protocols work.

Sample Output

Router Network - Bellman-Ford Shortest Paths
Source: Router A

Router B: distance = 5ms, via Router A
Router C: distance = 1ms, via Router A
Router D: distance = 6.3ms, via Router B
...
No negative cycles detected.

Key NWGraph Features Demonstrated

  • Edge iteration: Processing all edges for relaxation

  • Distance tracking: Recording shortest path distances

  • Predecessor maps: Building the shortest-paths tree

  • Negative cycle detection: Verifying solution validity

References

  • Cormen, Leiserson, Rivest, and Stein. Introduction to Algorithms, 4th Edition (2022), Chapter 22.1: The Bellman-Ford Algorithm

  • Siek, Lee, and Lumsdaine. The Boost Graph Library (2002), Chapter 5.3

See Also