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
Algorithm Library Functions in C++ STL
Non-modifying sequence operations std :: all_of : Test condition on all elements in rangestd :: any_of : Test if any element in range fulfills conditionstd :: none_of : Test if no elements fulfill conditionstd :: for_each : Apply function to rangestd :: find : Find value in rangestd :: find_if : Fin
4 min read
Boyer-Moore Algorithm for Pattern Searching in C++
The Boyer-Moore algorithm is an efficient string searching algorithm that is used to find occurrences of a pattern within a text. This algorithm preprocesses the pattern and uses this information to skip sections of the text, making it much faster than simpler algorithms like the naive approach.In t
6 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
STD::array in C++
The array is a collection of homogeneous objects and this array container is defined for constant size arrays or (static size). This container wraps around fixed-size arrays and the information of its size are not lost when declared to a pointer. In order to utilize arrays, we need to include the ar
5 min read
Array of Vectors in C++ STL
Prerequisite: Arrays in C++, Vector in C++ STL An array is a collection of items stored at contiguous memory locations. It is to store multiple items of the same type together. This makes it easier to get access to the elements stored in it by the position of each element. Vectors are known as dynam
3 min read
Vector begin() in C++ STL
In C++, the vector begin() is a built-in method used to obtain an iterator pointing to the start of the vector. This iterator is used to traverse the vector or perform operations starting from the beginning of the vector.Letâs take a look at an example that illustrates the use of the vector begin()
4 min read
Assignment Operators in C++
In C++, the assignment operator forms the backbone of computational processes by performing a simple operation like assigning a value to a variable. It is denoted by equal sign ( = ) and provides one of the most basic operations in any programming language i.e. assign some value to the variables in
6 min read
for_each loop in C++
Apart from the generic looping techniques, such as "for, while and do-while", C++ in its language also allows us to use another functionality which solves the same purpose termed "for-each" loops. This loop accepts a function which executes over each of the container elements. This loop is defined i
5 min read
std::string::assign() in C++
The member function assign() is used for the assignments, it assigns a new value to the string, replacing its current contents. Syntax 1: Assign the value of string str. string& string::assign (const string& str) str : is the string to be assigned. Returns : *this CPP // CPP code for assign
5 min read