Imagine you are working on a chemistry project. After numerous experiments, your fellow chemists have collected the necessary data and represented it in a graph as shown in the picture below. The nodes are the compounds, while the weights stand for the amount of energy needed to convert one node to another: if negative, energy is released during the reaction.
Given a certain type of reactant, your task is to obtain all compounds by spending as little energy as possible. In situations where negative weights are present, negative cycles can occur. The Bellman-Ford algorithm is designed to detect the presence of such cycles. In the contrary case, it finds the shortest paths from a given node to all other nodes of the graph.
Description of the algorithm
In a weighted directed graph , we need to find the shortest paths from the source node to all other nodes. denotes the weight of the edge . If there is no such edge, we assume . Let represent the weight of the shortest currently known path from node to node . If no such path has been found yet, we set . For the source itself, .
The main idea is to apply the process of relaxation along edges until necessary. Relaxing along the edge is an attempt to get a shorter path from to using the given edge. In other words, if , we update to a smaller value, namely , hence we include the edge in our shortest path. In this case, we call the relaxation process successful. Otherwise, the process is said to be unsuccessful, as we don't change anything.
In more detail, the Bellman-Ford algorithm can be described as follows:
- Initialize the distances: .
- Apply the process of relaxation to every edge exactly times, where represents the number of nodes in graph .
- After step 2, we already have the final values of shortest paths . However, there might be negative cycles. As we already noted, they can infinitely reduce the size of our shortest path, therefore it is necessary and sufficient to apply the relaxation process one last time. If any changes are observed in at least one of the shortest paths, we may confirm the presence of such cycle. Otherwise, we are done.
Why applications of relaxation are sufficient? It is not difficult to understand that after the application, the algorithm correctly finds all the shortest paths consisting of no more than edges. Bearing in mind that cycles cannot serve as shortest paths, every path consists of edges or less, therefore the algorithm works correctly.
Example 1: 'No bad cycles'
Let's get back to the graph we introduced at the beginning of this topic. Set node as the source node. Our task is to find the shortest paths from this node to all other vertices using the Bellman-Ford algorithm.
- Initialize the values of the shortest paths. We will write these values above/below the corresponding nodes.
- Fix some order in which the edges will be processed, say . Perform relaxation times. For the sake of better understanding, we will explain only the first few iterations in detail. We notice that . Therefore, we update the value of by setting it equal to . Analogously, we set , as while relaxing along we end up with . Some of the edges will contribute nothing to our relaxation process. For example, while relaxing along , we get . All in all, we get , as shown in picture 1.
- The second iteration is done similarly. For example, we get a shorter path to the fourth node, as . We continue until the fifth iteration is done. The distances for each iteration are shown in the pictures below:
- Just in case, we check for the presence of negative cycles. Perform relaxation once again. We see that none of the values of changes, hence no negative cycles are found in our graph. The algorithm returns the array of shortest paths . As we mentioned in the previous paragraph, it is quite easy to restore the paths themselves. Try it!
Example 2: 'Catch those negative cycles'
What if there are negative cycles? How does the algorithm detect them? Let's consider the following example:
We are given a graph consisting of nodes. As in the previous example, repeat relaxation times. Take the following ordering of edges: . The results are shown in the images below.
The final step: checking for our main enemy negative cycles. Perform relaxation one more time. We notice a change in the weight of the shortest path to nodes and .
This means that the algorithm has detected the presence of a negative cycle. Indeed, we can all see it standing right in front of us, can't we? The algorithm can be easily modified to explicitly return the cycle itself using the backtracking method.
Pseudocode
Having explored the Bellman-Ford algorithm in the preceding sections, we now delve into its practical implementation through pseudocode. The Bellman-Ford algorithm stands as a robust tool for finding shortest paths in weighted directed graphs, addressing even the challenge of negative cycles. Let's articulate its procedural details to enhance our understanding of its application in various scenarios.
// Function to perform Bellman-Ford algorithm on a weighted directed graph
function bellmanFord(graph, source):
// Step 1: Initialize distances
distances = new Map<Node, Integer>()
predecessors = new Map<Node, Node>()
for each node in graph.nodes:
distances.put(node, INFINITY)
predecessors.put(node, null)
distances.put(source, 0)
// Step 2: Relaxation process
for i in [1, graph.nodes.size() - 1]: // for loop iteration (|graph.nodes| - 1) times
for each edge in graph.edges:
if relax(edge) == false:
// Negative cycle detected, stop the algorithm
print("Negative cycle detected. Unable to find shortest paths.")
return
// Step 3: Check for negative cycles
for each edge in graph.edges:
if relax(edge) == true:
// Negative cycle detected
print("Negative cycle detected. Unable to find shortest paths.")
return
// Function to perform relaxation along an edge
function relax(edge):
u = edge.source
v = edge.target
weight = edge.weight
if distances[u] + weight < distances[v]:
distances[v] = distances[u] + weight
predecessors[v] = u
return true // Relaxation successful
else:
return false // Relaxation unsuccessful
// Function to restore the shortest path
function restorePath(node):
path = new List<Node>()
while node != null:
path.add(node)
node = predecessors[node]
return reverse(path)
// Example usage
sourceNode = graph.getNode(1)
bellmanFord(graph, sourceNode)Complexity Analysis
For a graph with nodes and edges, the time complexity of Bellman-Ford is . Indeed, we apply the process of relaxation times along every edge, i.e., times. Afterward, we do one more step of relaxation along every edge. Relaxation consists of a single comparison, therefore its complexity is . Overall, our algorithm will perform steps.
Conclusion
Here is a brief summary of what we discussed in this topic. The Bellman-Ford algorithm is a powerful shortest path solving tool. Unlike some other algorithms, it works correctly for graphs containing negative cycles as well. It consists of three easily realizable steps:
- Initialize the values of shortest paths.
- Apply the relaxation process times to get the final weights of shortest paths from the source to every other node.
- Relax along every edge one last time to check for negative cycles. If any changes are observed, confirm their presence.
For a more interactive example of how the algorithm works, check out a visualization.