Shortest Path Algorithms

Detailed study of shortest path algorithms, including Dijkstra's algorithm (for single-source shortest paths in weighted graphs with non-negative edge weights) and Bellman-Ford algorithm (for handling negative edge weights).


Bellman-Ford Algorithm

Introduction

The Bellman-Ford algorithm is a single-source shortest path algorithm used to find the shortest paths from a source vertex to all other vertices in a weighted graph. Unlike Dijkstra's algorithm, Bellman-Ford can handle graphs with negative edge weights. It also detects the presence of negative cycles within the graph. A negative cycle is a cycle in which the sum of the edge weights is negative, which would cause the shortest paths to be undefined, as one could traverse the cycle infinitely to reduce the path length.

Algorithm Explanation

The Bellman-Ford algorithm works by iteratively relaxing the edges of the graph. Relaxation is the process of updating the estimated shortest distance to a vertex if a shorter path is found through a neighboring vertex. It performs this relaxation step |V|-1 times, where |V| is the number of vertices in the graph. After these |V|-1 iterations, a final iteration is performed to check for negative cycles. If a negative cycle exists, the algorithm reports its presence.

Detailed Steps

  1. Initialization: For each vertex v in the graph, set its distance from the source vertex s, dist[v], to infinity (a very large number). Set dist[s] to 0.
  2. Relaxation: Repeat the following process |V|-1 times:
    • For each edge (u, v) with weight w:
      • If dist[u] + w < dist[v], then update dist[v] = dist[u] + w.
  3. Negative Cycle Detection: For each edge (u, v) with weight w:
    • If dist[u] + w < dist[v], then a negative cycle exists. The algorithm should report that a negative cycle has been detected and terminate.
  4. Result: If no negative cycle is detected, dist[v] contains the shortest distance from the source vertex s to vertex v for all v in the graph.

Pseudocode

  function BellmanFord(graph, source)

  // Step 1: Initialize distances from source to all vertices as INFINITE
  dist = array of size |V|
  for each vertex v in V(graph)
      dist[v] = INFINITY
  dist[source] = 0

  // Step 2: Relax edges repeatedly
  for i from 1 to |V| - 1
      for each edge (u, v) with weight w in E(graph)
          if dist[u] != INFINITY and dist[u] + w < dist[v]
              dist[v] = dist[u] + w

  // Step 3: Check for negative-weight cycles
  for each edge (u, v) with weight w in E(graph)
      if dist[u] != INFINITY and dist[u] + w < dist[v]
          print "Graph contains negative weight cycle"
          return  // Or raise an exception

  return dist 

Example Execution

Consider a graph with vertices A, B, C, and D, and the following edges with their respective weights:

  • (A, B) = -1
  • (A, C) = 4
  • (B, C) = 3
  • (B, D) = 2
  • (B, E) = 2
  • (D, B) = 1
  • (D, C) = 5
  • (E, D) = -3
  • (C, D) = -4

Let's run Bellman-Ford with source vertex A:

  1. Initialization: dist[A] = 0, dist[B] = INFINITY, dist[C] = INFINITY, dist[D] = INFINITY, dist[E] = INFINITY
  2. Iteration 1:
    • (A, B): dist[B] = 0 + (-1) = -1
    • (A, C): dist[C] = 0 + 4 = 4
  3. Iteration 2:
    • (A, B): dist[B] remains -1
    • (A, C): dist[C] remains 4
    • (B, C): dist[C] = -1 + 3 = 2 (updated)
    • (B, D): dist[D] = -1 + 2 = 1
    • (B, E): dist[E] = -1 + 2 = 1
  4. Iteration 3:
    • (A, B): dist[B] remains -1
    • (A, C): dist[C] remains 4
    • (B, C): dist[C] remains 2
    • (B, D): dist[D] remains 1
    • (B, E): dist[E] remains 1
    • (D, B): dist[B] = 1 + 1 = 2 (not updated because -1 is shorter)
    • (D, C): dist[C] = 1 + 5 = 6 (not updated because 2 is shorter)
    • (E, D): dist[D] = 1 + (-3) = -2
  5. Iteration 4:
    • (A, B): dist[B] remains -1
    • (A, C): dist[C] remains 4
    • (B, C): dist[C] remains 2
    • (B, D): dist[D] remains 1
    • (B, E): dist[E] remains 1
    • (D, B): dist[B] = -2 + 1 = -1 (not updated because -1 is shorter)
    • (D, C): dist[C] = -2 + 5 = 3 (not updated because 2 is shorter)
    • (E, D): dist[D] = 1 + (-3) = -2 (not updated because -2 is shorter)
    • (C, D): dist[D] = 2 + (-4) = -2 (not updated because -2 is shorter)
  6. Negative Cycle Detection: After |V|-1 = 4 iterations, we check for negative cycles. In this case, the graph does not contain any negative cycle.

Final shortest distances from A: dist[A] = 0, dist[B] = -1, dist[C] = 2, dist[D] = -2, dist[E] = 1

Negative Cycle Detection

The presence of negative cycles is detected by performing one additional relaxation pass *after* the |V|-1 iterations. If any distance dist[v] is further reduced during this pass, it indicates that there is a negative cycle reachable from the source vertex. This is because, in a graph without negative cycles, the shortest paths are guaranteed to be found within |V|-1 iterations. Further reduction implies that the algorithm has found an even *shorter* path, which can only occur if there's a path that loops back on itself with a negative weight sum.

If a negative cycle is found, the shortest path problem becomes ill-defined, as you can infinitely traverse the cycle to keep decreasing the path length. In practice, the algorithm would typically signal an error condition or return a special value to indicate the presence of a negative cycle.

Complexity Analysis

  • Time Complexity: O(|V| * |E|), where |V| is the number of vertices and |E| is the number of edges. This is because the outer loop iterates |V|-1 times, and the inner loop iterates over all edges.
  • Space Complexity: O(|V|), to store the distance values for each vertex.

Limitations

  • Inefficiency for Graphs without Negative Weights: Dijkstra's algorithm is generally more efficient than Bellman-Ford for graphs without negative edge weights. Dijkstra's has a time complexity of O(|E| + |V|log|V|) using a priority queue, which is generally better for sparse graphs.
  • Not Suitable for Very Large Graphs: The O(|V| * |E|) time complexity can be a bottleneck for very large graphs. Alternatives like using Johnson's algorithm (which combines Bellman-Ford with Dijkstra's) might be more efficient in certain cases when all-pairs shortest paths are needed.
  • Sensitivity to Edge Weight Representation: While generally robust, extreme negative weights can cause numerical instability in certain implementations. Care should be taken when handling very large magnitude negative weights.