diff --git a/graph/johnson.ts b/graph/johnson.ts new file mode 100644 index 00000000..7eac28ac --- /dev/null +++ b/graph/johnson.ts @@ -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; +} + diff --git a/graph/test/johnson.test.ts b/graph/test/johnson.test.ts new file mode 100644 index 00000000..24600879 --- /dev/null +++ b/graph/test/johnson.test.ts @@ -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); + }); +}) +