Skip to content

Commit

Permalink
feat: add johnson algorithm for all pairs shortest paths (#145)
Browse files Browse the repository at this point in the history
* feat: add johnson algorithm for all pairs shortest paths

* fix initializing edges. add test case for empty graph and disjoint
graph.

* more detail to test description

* Remove accidentally committed files
  • Loading branch information
caojoshua authored Jul 13, 2023
1 parent 03dc8b8 commit e8a850c
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 0 deletions.
52 changes: 52 additions & 0 deletions graph/johnson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { bellmanFord } from './bellman_ford'
import { dijkstra } from './dijkstra'

/**
* @function johnson
* @description Compute the shortest path for all pairs of nodes. The input graph is in adjacency list form. It is a multidimensional array of edges. graph[i] holds the edges for the i'th node. Each edge is a 2-tuple where the 0'th item is the destination node, and the 1'th item is the edge weight. Returned undefined if the graph has negative weighted cycles.
* @Complexity_Analysis
* Time complexity: O(VElog(V))
* Space Complexity: O(V^2) to hold the result
* @param {[number, number][][]} graph - The graph in adjacency list form
* @return {number[][]} - A matrix holding the shortest path for each pair of nodes. matrix[i][j] holds the distance of the shortest path (i -> j).
* @see https://en.wikipedia.org/wiki/Johnson%27s_algorithm
*/
export const johnson = (graph: [number, number][][]): number[][] | undefined => {
let N = graph.length;

// Add a new node and 0 weighted edges from the new node to all existing nodes.
let newNodeGraph = structuredClone(graph);
let newNode: [number, number][] = [];
for (let i = 0; i < N; ++i) {
newNode.push([i, 0]);
}
newNodeGraph.push(newNode);

// Compute distances from the new node to existing nodes using the Bellman-Ford algorithm.
let adjustedGraph = bellmanFord(newNodeGraph, N);
if (adjustedGraph === undefined) {
// Found a negative weight cycle.
return undefined;
}

for (let i = 0; i < N; ++i) {
for (let edge of graph[i]) {
// Adjust edge weights using the Bellman Ford output weights. This ensure that:
// 1. Each weight is non-negative. This is required for the Dijkstra algorithm.
// 2. The shortest path from node i to node j consists of the same nodes with or without adjustment.
edge[1] += adjustedGraph[i] - adjustedGraph[edge[0]];
}
}

let shortestPaths: number[][] = [];
for (let i = 0; i < N; ++i) {
// Compute Dijkstra weights for each node and re-adjust weights to their original values.
let dijkstraShorestPaths = dijkstra(graph, i);
for (let j = 0; j < N; ++j) {
dijkstraShorestPaths[j] += adjustedGraph[j] - adjustedGraph[i];
}
shortestPaths.push(dijkstraShorestPaths);
}
return shortestPaths;
}

107 changes: 107 additions & 0 deletions graph/test/johnson.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { johnson } from "../johnson";

describe("johnson", () => {

const init_graph = (N: number): [number, number][][] => {
let graph = Array(N);
for (let i = 0; i < N; ++i) {
graph[i] = [];
}
return graph;
}

const add_edge = (graph: [number, number][][], a: number, b: number, weight: number) => {
graph[a].push([b, weight]);
graph[b].push([a, weight]);
}

it("should return the correct value", () => {
let graph = init_graph(9);
add_edge(graph, 0, 1, 4);
add_edge(graph, 0, 7, 8);
add_edge(graph, 1, 2, 8);
add_edge(graph, 1, 7, 11);
add_edge(graph, 2, 3, 7);
add_edge(graph, 2, 5, 4);
add_edge(graph, 2, 8, 2);
add_edge(graph, 3, 4, 9);
add_edge(graph, 3, 5, 14);
add_edge(graph, 4, 5, 10);
add_edge(graph, 5, 6, 2);
add_edge(graph, 6, 7, 1);
add_edge(graph, 6, 8, 6);
add_edge(graph, 7, 8, 7);

let expected = [
[0, 4, 12, 19, 21, 11, 9, 8, 14],
[4, 0, 8, 15, 22, 12, 12, 11, 10],
[12, 8, 0, 7, 14, 4, 6, 7, 2],
[19, 15, 7, 0, 9, 11, 13, 14, 9],
[21, 22, 14, 9, 0, 10, 12, 13, 16],
[11, 12, 4, 11, 10, 0, 2, 3, 6],
[9, 12, 6, 13, 12, 2, 0, 1, 6],
[8, 11, 7, 14, 13, 3, 1, 0, 7],
[14, 10, 2, 9, 16, 6, 6, 7, 0]
]
expect(johnson(graph)).toStrictEqual(expected);
});

it("should return the correct value for graph with negative weights", () => {
let graph = init_graph(4);
graph[0].push([1, -5]);
graph[0].push([2, 2]);
graph[0].push([3, 3]);
graph[1].push([2, 4]);
graph[2].push([3, 1]);

let expected = [
[ 0, -5, -1, 0 ],
[ Infinity, 0, 4, 5 ],
[ Infinity, Infinity, 0, 1 ],
[ Infinity, Infinity, Infinity, 0 ]
]
expect(johnson(graph)).toStrictEqual(expected);
});

it("should return the undefined for two node graph with negative-weight cycle", () => {
let graph = init_graph(2);
add_edge(graph, 0, 1, -1);
expect(johnson(graph)).toStrictEqual(undefined);
});

it("should return the undefined for three node graph with negative-weight cycle", () => {
let graph = init_graph(3);
graph[0].push([1, -1]);
graph[0].push([2, 7]);
graph[1].push([2, -5]);
graph[2].push([0, 4]);
expect(johnson(graph)).toStrictEqual(undefined);
});

it("should return the correct value for zero element graph", () => {
expect(johnson([])).toStrictEqual([]);
});

it("should return the correct value for single element graph", () => {
expect(johnson([[]])).toStrictEqual([[0]]);
});

it("should return the correct value for a linear graph", () => {
let linear_graph = init_graph(4);
add_edge(linear_graph, 0, 1, 1);
add_edge(linear_graph, 1, 2, 2);
add_edge(linear_graph, 2, 3, 3);

let expected = [[0, 1, 3, 6 ], [1, 0, 2, 5], [3, 2, 0, 3], [6, 5, 3, 0]];
expect(johnson(linear_graph)).toStrictEqual(expected);
});

it("should return the correct value for a linear graph with unreachable node", () => {
let linear_graph = init_graph(3);
add_edge(linear_graph, 0, 1, 1);

let expected = [[0, 1, Infinity], [1, 0, Infinity], [Infinity, Infinity, 0]];
expect(johnson(linear_graph)).toStrictEqual(expected);
});
})

0 comments on commit e8a850c

Please sign in to comment.