Skip to content

feat: add johnson's algorithm #5712

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}