mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-07-07 17:56:02 +08:00
Add Johnson's algorithm (#5712)
This commit is contained in:
@ -0,0 +1,203 @@
|
|||||||
|
package com.thealgorithms.datastructures.graphs;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements Johnson's algorithm for finding all-pairs shortest paths in a weighted,
|
||||||
|
* directed graph that may contain negative edge weights.
|
||||||
|
*
|
||||||
|
* Johnson's algorithm works by using the Bellman-Ford algorithm to compute a transformation of the
|
||||||
|
* input graph that removes all negative weights, allowing Dijkstra's algorithm to be used for
|
||||||
|
* efficient shortest path computations.
|
||||||
|
*
|
||||||
|
* Time Complexity: O(V^2 * log(V) + V*E)
|
||||||
|
* Space Complexity: O(V^2)
|
||||||
|
*
|
||||||
|
* Where V is the number of vertices and E is the number of edges in the graph.
|
||||||
|
*
|
||||||
|
* For more information, please visit {@link https://en.wikipedia.org/wiki/Johnson%27s_algorithm}
|
||||||
|
*/
|
||||||
|
public final class JohnsonsAlgorithm {
|
||||||
|
|
||||||
|
// Constant representing infinity
|
||||||
|
private static final double INF = Double.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A private constructor to hide the implicit public one.
|
||||||
|
*/
|
||||||
|
private JohnsonsAlgorithm() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes Johnson's algorithm on the given graph.
|
||||||
|
*
|
||||||
|
* @param graph The input graph represented as an adjacency matrix.
|
||||||
|
* @return A 2D array representing the shortest distances between all pairs of vertices.
|
||||||
|
*/
|
||||||
|
public static double[][] johnsonAlgorithm(double[][] graph) {
|
||||||
|
int numVertices = graph.length;
|
||||||
|
double[][] edges = convertToEdgeList(graph);
|
||||||
|
|
||||||
|
// Step 1: Add a new vertex and run Bellman-Ford
|
||||||
|
double[] modifiedWeights = bellmanFord(edges, numVertices);
|
||||||
|
|
||||||
|
// Step 2: Reweight the graph
|
||||||
|
double[][] reweightedGraph = reweightGraph(graph, modifiedWeights);
|
||||||
|
|
||||||
|
// Step 3: Run Dijkstra's algorithm for each vertex
|
||||||
|
double[][] shortestDistances = new double[numVertices][numVertices];
|
||||||
|
for (int source = 0; source < numVertices; source++) {
|
||||||
|
shortestDistances[source] = dijkstra(reweightedGraph, source, modifiedWeights);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortestDistances;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the adjacency matrix representation of the graph to an edge list.
|
||||||
|
*
|
||||||
|
* @param graph The input graph as an adjacency matrix.
|
||||||
|
* @return An array of edges, where each edge is represented as [from, to, weight].
|
||||||
|
*/
|
||||||
|
public static double[][] convertToEdgeList(double[][] graph) {
|
||||||
|
int numVertices = graph.length;
|
||||||
|
List<double[]> edgeList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < numVertices; i++) {
|
||||||
|
for (int j = 0; j < numVertices; j++) {
|
||||||
|
if (i != j && !Double.isInfinite(graph[i][j])) {
|
||||||
|
// Only add edges that are not self-loops and have a finite weight
|
||||||
|
edgeList.add(new double[] {i, j, graph[i][j]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the List to a 2D array
|
||||||
|
return edgeList.toArray(new double[0][]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the Bellman-Ford algorithm to compute the shortest paths from a new vertex
|
||||||
|
* to all other vertices. This is used to calculate the weight function h(v) for reweighting.
|
||||||
|
*
|
||||||
|
* @param edges The edge list of the graph.
|
||||||
|
* @param numVertices The number of vertices in the original graph.
|
||||||
|
* @return An array of modified weights for each vertex.
|
||||||
|
*/
|
||||||
|
private static double[] bellmanFord(double[][] edges, int numVertices) {
|
||||||
|
double[] dist = new double[numVertices + 1];
|
||||||
|
Arrays.fill(dist, INF);
|
||||||
|
dist[numVertices] = 0; // Distance to the new source vertex is 0
|
||||||
|
|
||||||
|
// Add edges from the new vertex to all original vertices
|
||||||
|
double[][] allEdges = Arrays.copyOf(edges, edges.length + numVertices);
|
||||||
|
for (int i = 0; i < numVertices; i++) {
|
||||||
|
allEdges[edges.length + i] = new double[] {numVertices, i, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relax all edges V times
|
||||||
|
for (int i = 0; i < numVertices; i++) {
|
||||||
|
for (double[] edge : allEdges) {
|
||||||
|
int u = (int) edge[0];
|
||||||
|
int v = (int) edge[1];
|
||||||
|
double weight = edge[2];
|
||||||
|
if (dist[u] != INF && dist[u] + weight < dist[v]) {
|
||||||
|
dist[v] = dist[u] + weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for negative weight cycles
|
||||||
|
for (double[] edge : allEdges) {
|
||||||
|
int u = (int) edge[0];
|
||||||
|
int v = (int) edge[1];
|
||||||
|
double weight = edge[2];
|
||||||
|
if (dist[u] + weight < dist[v]) {
|
||||||
|
throw new IllegalArgumentException("Graph contains a negative weight cycle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Arrays.copyOf(dist, numVertices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reweights the graph using the modified weights computed by Bellman-Ford.
|
||||||
|
*
|
||||||
|
* @param graph The original graph.
|
||||||
|
* @param modifiedWeights The modified weights from Bellman-Ford.
|
||||||
|
* @return The reweighted graph.
|
||||||
|
*/
|
||||||
|
public static double[][] reweightGraph(double[][] graph, double[] modifiedWeights) {
|
||||||
|
int numVertices = graph.length;
|
||||||
|
double[][] reweightedGraph = new double[numVertices][numVertices];
|
||||||
|
|
||||||
|
for (int i = 0; i < numVertices; i++) {
|
||||||
|
for (int j = 0; j < numVertices; j++) {
|
||||||
|
if (graph[i][j] != 0) {
|
||||||
|
// New weight = original weight + h(u) - h(v)
|
||||||
|
reweightedGraph[i][j] = graph[i][j] + modifiedWeights[i] - modifiedWeights[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reweightedGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements Dijkstra's algorithm for finding shortest paths from a source vertex.
|
||||||
|
*
|
||||||
|
* @param reweightedGraph The reweighted graph to run Dijkstra's on.
|
||||||
|
* @param source The source vertex.
|
||||||
|
* @param modifiedWeights The modified weights from Bellman-Ford.
|
||||||
|
* @return An array of shortest distances from the source to all other vertices.
|
||||||
|
*/
|
||||||
|
public static double[] dijkstra(double[][] reweightedGraph, int source, double[] modifiedWeights) {
|
||||||
|
int numVertices = reweightedGraph.length;
|
||||||
|
double[] dist = new double[numVertices];
|
||||||
|
boolean[] visited = new boolean[numVertices];
|
||||||
|
Arrays.fill(dist, INF);
|
||||||
|
dist[source] = 0;
|
||||||
|
|
||||||
|
for (int count = 0; count < numVertices - 1; count++) {
|
||||||
|
int u = minDistance(dist, visited);
|
||||||
|
visited[u] = true;
|
||||||
|
|
||||||
|
for (int v = 0; v < numVertices; v++) {
|
||||||
|
if (!visited[v] && reweightedGraph[u][v] != 0 && dist[u] != INF && dist[u] + reweightedGraph[u][v] < dist[v]) {
|
||||||
|
dist[v] = dist[u] + reweightedGraph[u][v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust distances back to the original graph weights
|
||||||
|
for (int i = 0; i < numVertices; i++) {
|
||||||
|
if (dist[i] != INF) {
|
||||||
|
dist[i] = dist[i] - modifiedWeights[source] + modifiedWeights[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the vertex with the minimum distance value from the set of vertices
|
||||||
|
* not yet included in the shortest path tree.
|
||||||
|
*
|
||||||
|
* @param dist Array of distances.
|
||||||
|
* @param visited Array of visited vertices.
|
||||||
|
* @return The index of the vertex with minimum distance.
|
||||||
|
*/
|
||||||
|
public static int minDistance(double[] dist, boolean[] visited) {
|
||||||
|
double min = INF;
|
||||||
|
int minIndex = -1;
|
||||||
|
for (int v = 0; v < dist.length; v++) {
|
||||||
|
if (!visited[v] && dist[v] <= min) {
|
||||||
|
min = dist[v];
|
||||||
|
minIndex = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return minIndex;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
package com.thealgorithms.datastructures.graphs;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit tests for {@link JohnsonsAlgorithm} class. This class
|
||||||
|
* contains test cases to verify the correct implementation of
|
||||||
|
* various methods used in Johnson's Algorithm such as shortest path
|
||||||
|
* calculations, graph reweighting, and more.
|
||||||
|
*/
|
||||||
|
class JohnsonsAlgorithmTest {
|
||||||
|
|
||||||
|
// Constant representing infinity
|
||||||
|
private static final double INF = Double.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the Johnson's Algorithm with a simple graph without negative edges.
|
||||||
|
* Verifies that the algorithm returns the correct shortest path distances.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testSimpleGraph() {
|
||||||
|
// Test case for a simple graph without negative edges
|
||||||
|
double[][] graph = {{0, 4, INF, INF}, {INF, 0, 1, INF}, {INF, INF, 0, 2}, {INF, INF, INF, 0}};
|
||||||
|
|
||||||
|
double[][] result = JohnsonsAlgorithm.johnsonAlgorithm(graph);
|
||||||
|
|
||||||
|
double[][] expected = {{0, 4, 5, 7}, {INF, 0, 1, 3}, {INF, INF, 0, 2}, {INF, INF, INF, 0}};
|
||||||
|
|
||||||
|
assertArrayEquals(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests Johnson's Algorithm on a graph with negative edges but no
|
||||||
|
* negative weight cycles. Verifies the algorithm handles negative
|
||||||
|
* edge weights correctly.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testGraphWithNegativeEdges() {
|
||||||
|
// Graph with negative edges but no negative weight cycles
|
||||||
|
double[][] graph = {{0, -1, 4}, {INF, 0, 3}, {INF, INF, 0}};
|
||||||
|
|
||||||
|
double[][] result = JohnsonsAlgorithm.johnsonAlgorithm(graph);
|
||||||
|
|
||||||
|
double[][] expected = {{0, INF, 4}, {INF, 0, 3}, {INF, INF, 0}};
|
||||||
|
|
||||||
|
assertArrayEquals(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the behavior of Johnson's Algorithm on a graph with a negative
|
||||||
|
* weight cycle. Expects an IllegalArgumentException to be thrown
|
||||||
|
* due to the presence of the cycle.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNegativeWeightCycle() {
|
||||||
|
// Graph with a negative weight cycle
|
||||||
|
double[][] graph = {{0, 1, INF}, {INF, 0, -1}, {-1, INF, 0}};
|
||||||
|
|
||||||
|
// Johnson's algorithm should throw an exception when a negative cycle is detected
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> { JohnsonsAlgorithm.johnsonAlgorithm(graph); });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests Dijkstra's algorithm as a part of Johnson's algorithm implementation
|
||||||
|
* on a small graph. Verifies that the shortest path is correctly calculated.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testDijkstra() {
|
||||||
|
// Testing Dijkstra's algorithm with a small graph
|
||||||
|
double[][] graph = {{0, 1, 2}, {INF, 0, 3}, {INF, INF, 0}};
|
||||||
|
|
||||||
|
double[] modifiedWeights = {0, 0, 0}; // No reweighting in this simple case
|
||||||
|
|
||||||
|
double[] result = JohnsonsAlgorithm.dijkstra(graph, 0, modifiedWeights);
|
||||||
|
double[] expected = {0, 1, 2};
|
||||||
|
|
||||||
|
assertArrayEquals(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the conversion of an adjacency matrix to an edge list.
|
||||||
|
* Verifies that the conversion process generates the correct edge list.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testEdgeListConversion() {
|
||||||
|
// Test the conversion of adjacency matrix to edge list
|
||||||
|
double[][] graph = {{0, 5, INF}, {INF, 0, 2}, {INF, INF, 0}};
|
||||||
|
|
||||||
|
// Running convertToEdgeList
|
||||||
|
double[][] edges = JohnsonsAlgorithm.convertToEdgeList(graph);
|
||||||
|
|
||||||
|
// Expected edge list: (0 -> 1, weight 5), (1 -> 2, weight 2)
|
||||||
|
double[][] expected = {{0, 1, 5}, {1, 2, 2}};
|
||||||
|
|
||||||
|
// Verify the edge list matches the expected values
|
||||||
|
assertArrayEquals(expected, edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the reweighting of a graph as a part of Johnson's Algorithm.
|
||||||
|
* Verifies that the reweighted graph produces correct results.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testReweightGraph() {
|
||||||
|
// Test reweighting of the graph
|
||||||
|
double[][] graph = {{0, 2, 9}, {INF, 0, 1}, {INF, INF, 0}};
|
||||||
|
double[] modifiedWeights = {1, 2, 3}; // Arbitrary weight function
|
||||||
|
|
||||||
|
double[][] reweightedGraph = JohnsonsAlgorithm.reweightGraph(graph, modifiedWeights);
|
||||||
|
|
||||||
|
// Expected reweighted graph:
|
||||||
|
double[][] expected = {{0, 1, 7}, {INF, 0, 0}, {INF, INF, 0}};
|
||||||
|
|
||||||
|
assertArrayEquals(expected, reweightedGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the minDistance method used in Dijkstra's algorithm to find
|
||||||
|
* the vertex with the minimum distance that has not yet been visited.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testMinDistance() {
|
||||||
|
// Test minDistance method
|
||||||
|
double[] dist = {INF, 3, 1, INF};
|
||||||
|
boolean[] visited = {false, false, false, false};
|
||||||
|
|
||||||
|
int minIndex = JohnsonsAlgorithm.minDistance(dist, visited);
|
||||||
|
|
||||||
|
// The vertex with minimum distance is vertex 2 with a distance of 1
|
||||||
|
assertEquals(2, minIndex);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user