Johnson’s Algorithm is an algorithm used to find the shortest paths between all pairs of vertices in a weighted graph. It is especially useful for sparse graphs and can handle negative weights, provided there are no negative weight cycles. This algorithm uses both Bellman-Ford and Dijkstra's algorithms to achieve efficient results.
In this article, we will learn Johnson’s Algorithm and demonstrate how to implement it in C++.
Johnson’s Algorithm transforms the original graph to ensure all edge weights are non-negative, making it possible to use Dijkstra’s algorithm. The transformation involves adding a new vertex, computing a potential function using Bellman-Ford, re-weighting the edges, and then running Dijkstra’s algorithm from each vertex.
Steps to Implement Johnson’s Algorithm in C++
- Let the given graph be G. Add a new vertex s to the graph, add edges from the new vertex to all vertices of G. Let the modified graph be G’.
- Run the Bellman-Ford algorithm on G’ with s as the source. Let the distances calculated by Bellman-Ford be h[0], h[1], .. h[V-1]. If we find a negative weight cycle, then return. Note that the negative weight cycle cannot be created by new vertex s as there is no edge to s. All edges are from s.
- Reweight the edges of the original graph. For each edge (u, v), assign the new weight as “original weight + h[u] – h[v]”.
- Remove the added vertex s and run Dijkstra’s algorithm for every vertex.
Working of Johnson Algorithm in C++
Consider the below example to understand step-by-step implementation of Johnson’s Algorithm.
Step 1: Add a New Vertex
We add a new vertex 4 and connect it to all other vertices with edges of weight 0.
Step 2: Compute Potential Function using Bellman-Ford
Using the Bellman-Ford algorithm from vertex 4, we compute the shortest path estimates (potential function):
Distances from vertex 4 to vertices 0, 1, 2, and 3 are 0, −5, −1, and 0, respectively.
Therefore, h[]={0,−5,−1,0}
Step 3: Re-weight the Edges
Re-weight the edges using the formula
w′(u,v) = w(u,v) + h[u] − h[v]
After re-weighting, the edges are as follows:
Edge (0,1): Original weight = −5, New weight = −5+0−(−5) = 0
Edge (1,2): Original weight = 4, New weight = 4+(−5)−(−1) = 0
Edge (2,3): Original weight = 1, New weight = 1+(−1)−0 = 0
Edge (0,3): Original weight = 3, New weight = 3+0−0 = 3
Edge (3,2): Original weight = 2, New weight = 2+0−(−1) = 3
Step 4: Run Dijkstra's Algorithm
Run Dijkstra's algorithm from each vertex in the re-weighted graph to find the shortest paths.
Since all re-weighted edge weights are non-negative, Dijkstra's algorithm can be used efficiently.
C++ Program to Implement Johnson’s Algorithm
The below program illustrate the implementation of Johnson’s Algorithm in C++.
C++
// C++ program to implement Johnson's Algorithm for finding the shortest paths
// between all pairs of vertices in a graph that may contain negative weights.
#include <algorithm>
#include <iostream>
#include <limits>
#include <vector>
#define INF numeric_limits<int>::max()
using namespace std;
// Function to find the vertex with the minimum distance that has not yet been included in the shortest path
// tree
int Min_Distance(const vector<int> &dist, const vector<bool> &visited)
{
int min = INF, min_index;
for (int v = 0; v < dist.size(); ++v)
{
if (!visited[v] && dist[v] <= min)
{
min = dist[v];
min_index = v;
}
}
return min_index;
}
// Function to perform Dijkstra's algorithm on the modified graph
void Dijkstra_Algorithm(const vector<vector<int>> &graph, const vector<vector<int>> &altered_graph,
int source)
{
// Number of vertices
int V = graph.size();
// Distance from source to each vertex
vector<int> dist(V, INF);
// Track visited vertices
vector<bool> visited(V, false);
// Distance to source itself is 0
dist[source] = 0;
// Compute shortest path for all vertices
for (int count = 0; count < V - 1; ++count)
{
// Select the vertex with the minimum distance that hasn't been visited
int u = Min_Distance(dist, visited);
// Mark this vertex as visited
visited[u] = true;
// Update the distance value of the adjacent vertices of the selected vertex
for (int v = 0; v < V; ++v)
{
if (!visited[v] && graph[u][v] != 0 && dist[u] != INF && dist[u] + altered_graph[u][v] < dist[v])
{
dist[v] = dist[u] + altered_graph[u][v];
}
}
}
// Print the shortest distances from the source
cout << "Shortest Distance from vertex " << source << ":\n";
for (int i = 0; i < V; ++i)
{
cout << "Vertex " << i << ": " << (dist[i] == INF ? "INF" : to_string(dist[i])) << endl;
}
}
// Function to perform Bellman-Ford algorithm to find shortest distances
// from a source vertex to all other vertices
vector<int> BellmanFord_Algorithm(const vector<vector<int>> &edges, int V)
{
// Distance from source to each vertex
vector<int> dist(V + 1, INF);
// Distance to the new source vertex (added vertex) is 0
dist[V] = 0;
// Add a new source vertex to the graph and connect it to all original vertices with 0 weight edges
vector<vector<int>> edges_with_extra(edges);
for (int i = 0; i < V; ++i)
{
edges_with_extra.push_back({V, i, 0});
}
// Relax all edges |V| - 1 times
for (int i = 0; i < V; ++i)
{
for (const auto &edge : edges_with_extra)
{
if (dist[edge[0]] != INF && dist[edge[0]] + edge[2] < dist[edge[1]])
{
dist[edge[1]] = dist[edge[0]] + edge[2];
}
}
}
// Return distances excluding the new source vertex
return vector<int>(dist.begin(), dist.begin() + V);
}
// Function to implement Johnson's Algorithm
void JohnsonAlgorithm(const vector<vector<int>> &graph)
{
// Number of vertices
int V = graph.size();
vector<vector<int>> edges;
// Collect all edges from the graph
for (int i = 0; i < V; ++i)
{
for (int j = 0; j < V; ++j)
{
if (graph[i][j] != 0)
{
edges.push_back({i, j, graph[i][j]});
}
}
}
// Get the modified weights from Bellman-Ford algorithm
vector<int> altered_weights = BellmanFord_Algorithm(edges, V);
vector<vector<int>> altered_graph(V, vector<int>(V, 0));
// Modify the weights of the edges to remove negative weights
for (int i = 0; i < V; ++i)
{
for (int j = 0; j < V; ++j)
{
if (graph[i][j] != 0)
{
altered_graph[i][j] = graph[i][j] + altered_weights[i] - altered_weights[j];
}
}
}
// Print the modified graph with re-weighted edges
cout << "Modified Graph:\n";
for (const auto &row : altered_graph)
{
for (int weight : row)
{
cout << weight << ' ';
}
cout << endl;
}
// Run Dijkstra's algorithm for every vertex as the source
for (int source = 0; source < V; ++source)
{
cout << "\nShortest Distance with vertex " << source << " as the source:\n";
Dijkstra_Algorithm(graph, altered_graph, source);
}
}
// Main function to test the Johnson's Algorithm implementation
int main()
{
// Define a graph with possible negative weights
vector<vector<int>> graph = {{0, -5, 2, 3}, {0, 0, 4, 0}, {0, 0, 0, 1}, {0, 0, 0, 0}};
// Execute Johnson's Algorithm
JohnsonAlgorithm(graph);
return 0;
}
OutputModified Graph:
0 0 3 3
0 0 0 0
0 0 0 0
0 0 0 0
Shortest Distance with vertex 0 as the source:
Shortest Distance from vertex 0:
Vertex 0: 0
Vertex 1: 0
Vertex 2: 0
Vertex 3: 0
Shortest Distance with vertex 1 as the source:
Shortest Distance from vertex 1:
Vertex 0: INF
Vertex 1: 0
Vertex 2: 0
Vertex 3: 0
Shortest Distance with vertex 2 as the source:
Shortest Distance from vertex 2:
Vertex 0: INF
Vertex 1: INF
Vertex 2: 0
Vertex 3: 0
Shortest Distance with vertex 3 as the source:
Shortest Distance from vertex 3:
Vertex 0: INF
Vertex 1: INF
Vertex 2: INF
Vertex 3: 0
Time Complexity: The main steps in the algorithm are Bellman-Ford Algorithm called once and Dijkstra called V times. Time complexity of Bellman Ford is O(VE) and time complexity of Dijkstra is O(VLogV). So overall time complexity is O(V2log V + VE).
The time complexity of Johnson’s algorithm becomes the same as Floyd Warshall’s Algorithm
when the graph is complete (For a complete graph E = O(V2). But for sparse graphs, the algorithm performs much better than Floyd Warshall’s Algorithm.
Auxiliary Space: O(V2)
Similar Reads
Johnson Algorithm in C Johnson's Algorithm is an efficient algorithm used to find the shortest paths between all pairs of vertices in a weighted graph. It works even for graphs with negative weights, provided there are no negative weight cycles. This algorithm is particularly useful for sparse graphs and combines both Dij
5 min read
array::at() in C++ STL Array classes are generally more efficient, light-weight and reliable than C-style arrays. The introduction of array class from C++11 has offered a better alternative for C-style arrays. array::at() This function is used to return the reference to the element present at the position given as the par
2 min read
Basic Input / Output in C++ In C++, input and output are performed in the form of a sequence of bytes or more commonly known as streams.Input Stream: If the direction of flow of bytes is from the device (for example, Keyboard) to the main memory then this process is called input.Output Stream: If the direction of flow of bytes
5 min read
Basic Input / Output in C++ In C++, input and output are performed in the form of a sequence of bytes or more commonly known as streams.Input Stream: If the direction of flow of bytes is from the device (for example, Keyboard) to the main memory then this process is called input.Output Stream: If the direction of flow of bytes
5 min read
transform() in C++ STL In C++, transform() is a built-in STL function used to apply the given operation to a range of elements and store the result in another range. Letâs take a look at a simple example that shows the how to use this function:C++#include <bits/stdc++.h> using namespace std; int main() { vector<i
4 min read
find() in C++ STL C++ find() is a built-in function used to find the first occurrence of an element in the given range. It works with any container that supports iterators, such as arrays, vectors, lists, and more. In this article, we will learn about find() function in C++.C++#include <bits/stdc++.h> using nam
2 min read