Skip to content

Commit ac65af4

Browse files
Add Johnson's algorithm (#5712)
1 parent 31de2db commit ac65af4

File tree

2 files changed

+339
-0
lines changed

2 files changed

+339
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package com.thealgorithms.datastructures.graphs;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
7+
/**
8+
* This class implements Johnson's algorithm for finding all-pairs shortest paths in a weighted,
9+
* directed graph that may contain negative edge weights.
10+
*
11+
* Johnson's algorithm works by using the Bellman-Ford algorithm to compute a transformation of the
12+
* input graph that removes all negative weights, allowing Dijkstra's algorithm to be used for
13+
* efficient shortest path computations.
14+
*
15+
* Time Complexity: O(V^2 * log(V) + V*E)
16+
* Space Complexity: O(V^2)
17+
*
18+
* Where V is the number of vertices and E is the number of edges in the graph.
19+
*
20+
* For more information, please visit {@link https://en.wikipedia.org/wiki/Johnson%27s_algorithm}
21+
*/
22+
public final class JohnsonsAlgorithm {
23+
24+
// Constant representing infinity
25+
private static final double INF = Double.POSITIVE_INFINITY;
26+
27+
/**
28+
* A private constructor to hide the implicit public one.
29+
*/
30+
private JohnsonsAlgorithm() {
31+
}
32+
33+
/**
34+
* Executes Johnson's algorithm on the given graph.
35+
*
36+
* @param graph The input graph represented as an adjacency matrix.
37+
* @return A 2D array representing the shortest distances between all pairs of vertices.
38+
*/
39+
public static double[][] johnsonAlgorithm(double[][] graph) {
40+
int numVertices = graph.length;
41+
double[][] edges = convertToEdgeList(graph);
42+
43+
// Step 1: Add a new vertex and run Bellman-Ford
44+
double[] modifiedWeights = bellmanFord(edges, numVertices);
45+
46+
// Step 2: Reweight the graph
47+
double[][] reweightedGraph = reweightGraph(graph, modifiedWeights);
48+
49+
// Step 3: Run Dijkstra's algorithm for each vertex
50+
double[][] shortestDistances = new double[numVertices][numVertices];
51+
for (int source = 0; source < numVertices; source++) {
52+
shortestDistances[source] = dijkstra(reweightedGraph, source, modifiedWeights);
53+
}
54+
55+
return shortestDistances;
56+
}
57+
58+
/**
59+
* Converts the adjacency matrix representation of the graph to an edge list.
60+
*
61+
* @param graph The input graph as an adjacency matrix.
62+
* @return An array of edges, where each edge is represented as [from, to, weight].
63+
*/
64+
public static double[][] convertToEdgeList(double[][] graph) {
65+
int numVertices = graph.length;
66+
List<double[]> edgeList = new ArrayList<>();
67+
68+
for (int i = 0; i < numVertices; i++) {
69+
for (int j = 0; j < numVertices; j++) {
70+
if (i != j && !Double.isInfinite(graph[i][j])) {
71+
// Only add edges that are not self-loops and have a finite weight
72+
edgeList.add(new double[] {i, j, graph[i][j]});
73+
}
74+
}
75+
}
76+
77+
// Convert the List to a 2D array
78+
return edgeList.toArray(new double[0][]);
79+
}
80+
81+
/**
82+
* Implements the Bellman-Ford algorithm to compute the shortest paths from a new vertex
83+
* to all other vertices. This is used to calculate the weight function h(v) for reweighting.
84+
*
85+
* @param edges The edge list of the graph.
86+
* @param numVertices The number of vertices in the original graph.
87+
* @return An array of modified weights for each vertex.
88+
*/
89+
private static double[] bellmanFord(double[][] edges, int numVertices) {
90+
double[] dist = new double[numVertices + 1];
91+
Arrays.fill(dist, INF);
92+
dist[numVertices] = 0; // Distance to the new source vertex is 0
93+
94+
// Add edges from the new vertex to all original vertices
95+
double[][] allEdges = Arrays.copyOf(edges, edges.length + numVertices);
96+
for (int i = 0; i < numVertices; i++) {
97+
allEdges[edges.length + i] = new double[] {numVertices, i, 0};
98+
}
99+
100+
// Relax all edges V times
101+
for (int i = 0; i < numVertices; i++) {
102+
for (double[] edge : allEdges) {
103+
int u = (int) edge[0];
104+
int v = (int) edge[1];
105+
double weight = edge[2];
106+
if (dist[u] != INF && dist[u] + weight < dist[v]) {
107+
dist[v] = dist[u] + weight;
108+
}
109+
}
110+
}
111+
112+
// Check for negative weight cycles
113+
for (double[] edge : allEdges) {
114+
int u = (int) edge[0];
115+
int v = (int) edge[1];
116+
double weight = edge[2];
117+
if (dist[u] + weight < dist[v]) {
118+
throw new IllegalArgumentException("Graph contains a negative weight cycle");
119+
}
120+
}
121+
122+
return Arrays.copyOf(dist, numVertices);
123+
}
124+
125+
/**
126+
* Reweights the graph using the modified weights computed by Bellman-Ford.
127+
*
128+
* @param graph The original graph.
129+
* @param modifiedWeights The modified weights from Bellman-Ford.
130+
* @return The reweighted graph.
131+
*/
132+
public static double[][] reweightGraph(double[][] graph, double[] modifiedWeights) {
133+
int numVertices = graph.length;
134+
double[][] reweightedGraph = new double[numVertices][numVertices];
135+
136+
for (int i = 0; i < numVertices; i++) {
137+
for (int j = 0; j < numVertices; j++) {
138+
if (graph[i][j] != 0) {
139+
// New weight = original weight + h(u) - h(v)
140+
reweightedGraph[i][j] = graph[i][j] + modifiedWeights[i] - modifiedWeights[j];
141+
}
142+
}
143+
}
144+
145+
return reweightedGraph;
146+
}
147+
148+
/**
149+
* Implements Dijkstra's algorithm for finding shortest paths from a source vertex.
150+
*
151+
* @param reweightedGraph The reweighted graph to run Dijkstra's on.
152+
* @param source The source vertex.
153+
* @param modifiedWeights The modified weights from Bellman-Ford.
154+
* @return An array of shortest distances from the source to all other vertices.
155+
*/
156+
public static double[] dijkstra(double[][] reweightedGraph, int source, double[] modifiedWeights) {
157+
int numVertices = reweightedGraph.length;
158+
double[] dist = new double[numVertices];
159+
boolean[] visited = new boolean[numVertices];
160+
Arrays.fill(dist, INF);
161+
dist[source] = 0;
162+
163+
for (int count = 0; count < numVertices - 1; count++) {
164+
int u = minDistance(dist, visited);
165+
visited[u] = true;
166+
167+
for (int v = 0; v < numVertices; v++) {
168+
if (!visited[v] && reweightedGraph[u][v] != 0 && dist[u] != INF && dist[u] + reweightedGraph[u][v] < dist[v]) {
169+
dist[v] = dist[u] + reweightedGraph[u][v];
170+
}
171+
}
172+
}
173+
174+
// Adjust distances back to the original graph weights
175+
for (int i = 0; i < numVertices; i++) {
176+
if (dist[i] != INF) {
177+
dist[i] = dist[i] - modifiedWeights[source] + modifiedWeights[i];
178+
}
179+
}
180+
181+
return dist;
182+
}
183+
184+
/**
185+
* Finds the vertex with the minimum distance value from the set of vertices
186+
* not yet included in the shortest path tree.
187+
*
188+
* @param dist Array of distances.
189+
* @param visited Array of visited vertices.
190+
* @return The index of the vertex with minimum distance.
191+
*/
192+
public static int minDistance(double[] dist, boolean[] visited) {
193+
double min = INF;
194+
int minIndex = -1;
195+
for (int v = 0; v < dist.length; v++) {
196+
if (!visited[v] && dist[v] <= min) {
197+
min = dist[v];
198+
minIndex = v;
199+
}
200+
}
201+
return minIndex;
202+
}
203+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package com.thealgorithms.datastructures.graphs;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
/**
10+
* Unit tests for {@link JohnsonsAlgorithm} class. This class
11+
* contains test cases to verify the correct implementation of
12+
* various methods used in Johnson's Algorithm such as shortest path
13+
* calculations, graph reweighting, and more.
14+
*/
15+
class JohnsonsAlgorithmTest {
16+
17+
// Constant representing infinity
18+
private static final double INF = Double.POSITIVE_INFINITY;
19+
20+
/**
21+
* Tests the Johnson's Algorithm with a simple graph without negative edges.
22+
* Verifies that the algorithm returns the correct shortest path distances.
23+
*/
24+
@Test
25+
void testSimpleGraph() {
26+
// Test case for a simple graph without negative edges
27+
double[][] graph = {{0, 4, INF, INF}, {INF, 0, 1, INF}, {INF, INF, 0, 2}, {INF, INF, INF, 0}};
28+
29+
double[][] result = JohnsonsAlgorithm.johnsonAlgorithm(graph);
30+
31+
double[][] expected = {{0, 4, 5, 7}, {INF, 0, 1, 3}, {INF, INF, 0, 2}, {INF, INF, INF, 0}};
32+
33+
assertArrayEquals(expected, result);
34+
}
35+
36+
/**
37+
* Tests Johnson's Algorithm on a graph with negative edges but no
38+
* negative weight cycles. Verifies the algorithm handles negative
39+
* edge weights correctly.
40+
*/
41+
@Test
42+
void testGraphWithNegativeEdges() {
43+
// Graph with negative edges but no negative weight cycles
44+
double[][] graph = {{0, -1, 4}, {INF, 0, 3}, {INF, INF, 0}};
45+
46+
double[][] result = JohnsonsAlgorithm.johnsonAlgorithm(graph);
47+
48+
double[][] expected = {{0, INF, 4}, {INF, 0, 3}, {INF, INF, 0}};
49+
50+
assertArrayEquals(expected, result);
51+
}
52+
53+
/**
54+
* Tests the behavior of Johnson's Algorithm on a graph with a negative
55+
* weight cycle. Expects an IllegalArgumentException to be thrown
56+
* due to the presence of the cycle.
57+
*/
58+
@Test
59+
void testNegativeWeightCycle() {
60+
// Graph with a negative weight cycle
61+
double[][] graph = {{0, 1, INF}, {INF, 0, -1}, {-1, INF, 0}};
62+
63+
// Johnson's algorithm should throw an exception when a negative cycle is detected
64+
assertThrows(IllegalArgumentException.class, () -> { JohnsonsAlgorithm.johnsonAlgorithm(graph); });
65+
}
66+
67+
/**
68+
* Tests Dijkstra's algorithm as a part of Johnson's algorithm implementation
69+
* on a small graph. Verifies that the shortest path is correctly calculated.
70+
*/
71+
@Test
72+
void testDijkstra() {
73+
// Testing Dijkstra's algorithm with a small graph
74+
double[][] graph = {{0, 1, 2}, {INF, 0, 3}, {INF, INF, 0}};
75+
76+
double[] modifiedWeights = {0, 0, 0}; // No reweighting in this simple case
77+
78+
double[] result = JohnsonsAlgorithm.dijkstra(graph, 0, modifiedWeights);
79+
double[] expected = {0, 1, 2};
80+
81+
assertArrayEquals(expected, result);
82+
}
83+
84+
/**
85+
* Tests the conversion of an adjacency matrix to an edge list.
86+
* Verifies that the conversion process generates the correct edge list.
87+
*/
88+
@Test
89+
void testEdgeListConversion() {
90+
// Test the conversion of adjacency matrix to edge list
91+
double[][] graph = {{0, 5, INF}, {INF, 0, 2}, {INF, INF, 0}};
92+
93+
// Running convertToEdgeList
94+
double[][] edges = JohnsonsAlgorithm.convertToEdgeList(graph);
95+
96+
// Expected edge list: (0 -> 1, weight 5), (1 -> 2, weight 2)
97+
double[][] expected = {{0, 1, 5}, {1, 2, 2}};
98+
99+
// Verify the edge list matches the expected values
100+
assertArrayEquals(expected, edges);
101+
}
102+
103+
/**
104+
* Tests the reweighting of a graph as a part of Johnson's Algorithm.
105+
* Verifies that the reweighted graph produces correct results.
106+
*/
107+
@Test
108+
void testReweightGraph() {
109+
// Test reweighting of the graph
110+
double[][] graph = {{0, 2, 9}, {INF, 0, 1}, {INF, INF, 0}};
111+
double[] modifiedWeights = {1, 2, 3}; // Arbitrary weight function
112+
113+
double[][] reweightedGraph = JohnsonsAlgorithm.reweightGraph(graph, modifiedWeights);
114+
115+
// Expected reweighted graph:
116+
double[][] expected = {{0, 1, 7}, {INF, 0, 0}, {INF, INF, 0}};
117+
118+
assertArrayEquals(expected, reweightedGraph);
119+
}
120+
121+
/**
122+
* Tests the minDistance method used in Dijkstra's algorithm to find
123+
* the vertex with the minimum distance that has not yet been visited.
124+
*/
125+
@Test
126+
void testMinDistance() {
127+
// Test minDistance method
128+
double[] dist = {INF, 3, 1, INF};
129+
boolean[] visited = {false, false, false, false};
130+
131+
int minIndex = JohnsonsAlgorithm.minDistance(dist, visited);
132+
133+
// The vertex with minimum distance is vertex 2 with a distance of 1
134+
assertEquals(2, minIndex);
135+
}
136+
}

0 commit comments

Comments
 (0)