Skip to content

Commit e8a850c

Browse files
authored
feat: add johnson algorithm for all pairs shortest paths (#145)
* 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
1 parent 03dc8b8 commit e8a850c

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

graph/johnson.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { bellmanFord } from './bellman_ford'
2+
import { dijkstra } from './dijkstra'
3+
4+
/**
5+
* @function johnson
6+
* @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.
7+
* @Complexity_Analysis
8+
* Time complexity: O(VElog(V))
9+
* Space Complexity: O(V^2) to hold the result
10+
* @param {[number, number][][]} graph - The graph in adjacency list form
11+
* @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).
12+
* @see https://en.wikipedia.org/wiki/Johnson%27s_algorithm
13+
*/
14+
export const johnson = (graph: [number, number][][]): number[][] | undefined => {
15+
let N = graph.length;
16+
17+
// Add a new node and 0 weighted edges from the new node to all existing nodes.
18+
let newNodeGraph = structuredClone(graph);
19+
let newNode: [number, number][] = [];
20+
for (let i = 0; i < N; ++i) {
21+
newNode.push([i, 0]);
22+
}
23+
newNodeGraph.push(newNode);
24+
25+
// Compute distances from the new node to existing nodes using the Bellman-Ford algorithm.
26+
let adjustedGraph = bellmanFord(newNodeGraph, N);
27+
if (adjustedGraph === undefined) {
28+
// Found a negative weight cycle.
29+
return undefined;
30+
}
31+
32+
for (let i = 0; i < N; ++i) {
33+
for (let edge of graph[i]) {
34+
// Adjust edge weights using the Bellman Ford output weights. This ensure that:
35+
// 1. Each weight is non-negative. This is required for the Dijkstra algorithm.
36+
// 2. The shortest path from node i to node j consists of the same nodes with or without adjustment.
37+
edge[1] += adjustedGraph[i] - adjustedGraph[edge[0]];
38+
}
39+
}
40+
41+
let shortestPaths: number[][] = [];
42+
for (let i = 0; i < N; ++i) {
43+
// Compute Dijkstra weights for each node and re-adjust weights to their original values.
44+
let dijkstraShorestPaths = dijkstra(graph, i);
45+
for (let j = 0; j < N; ++j) {
46+
dijkstraShorestPaths[j] += adjustedGraph[j] - adjustedGraph[i];
47+
}
48+
shortestPaths.push(dijkstraShorestPaths);
49+
}
50+
return shortestPaths;
51+
}
52+

graph/test/johnson.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { johnson } from "../johnson";
2+
3+
describe("johnson", () => {
4+
5+
const init_graph = (N: number): [number, number][][] => {
6+
let graph = Array(N);
7+
for (let i = 0; i < N; ++i) {
8+
graph[i] = [];
9+
}
10+
return graph;
11+
}
12+
13+
const add_edge = (graph: [number, number][][], a: number, b: number, weight: number) => {
14+
graph[a].push([b, weight]);
15+
graph[b].push([a, weight]);
16+
}
17+
18+
it("should return the correct value", () => {
19+
let graph = init_graph(9);
20+
add_edge(graph, 0, 1, 4);
21+
add_edge(graph, 0, 7, 8);
22+
add_edge(graph, 1, 2, 8);
23+
add_edge(graph, 1, 7, 11);
24+
add_edge(graph, 2, 3, 7);
25+
add_edge(graph, 2, 5, 4);
26+
add_edge(graph, 2, 8, 2);
27+
add_edge(graph, 3, 4, 9);
28+
add_edge(graph, 3, 5, 14);
29+
add_edge(graph, 4, 5, 10);
30+
add_edge(graph, 5, 6, 2);
31+
add_edge(graph, 6, 7, 1);
32+
add_edge(graph, 6, 8, 6);
33+
add_edge(graph, 7, 8, 7);
34+
35+
let expected = [
36+
[0, 4, 12, 19, 21, 11, 9, 8, 14],
37+
[4, 0, 8, 15, 22, 12, 12, 11, 10],
38+
[12, 8, 0, 7, 14, 4, 6, 7, 2],
39+
[19, 15, 7, 0, 9, 11, 13, 14, 9],
40+
[21, 22, 14, 9, 0, 10, 12, 13, 16],
41+
[11, 12, 4, 11, 10, 0, 2, 3, 6],
42+
[9, 12, 6, 13, 12, 2, 0, 1, 6],
43+
[8, 11, 7, 14, 13, 3, 1, 0, 7],
44+
[14, 10, 2, 9, 16, 6, 6, 7, 0]
45+
]
46+
expect(johnson(graph)).toStrictEqual(expected);
47+
});
48+
49+
it("should return the correct value for graph with negative weights", () => {
50+
let graph = init_graph(4);
51+
graph[0].push([1, -5]);
52+
graph[0].push([2, 2]);
53+
graph[0].push([3, 3]);
54+
graph[1].push([2, 4]);
55+
graph[2].push([3, 1]);
56+
57+
let expected = [
58+
[ 0, -5, -1, 0 ],
59+
[ Infinity, 0, 4, 5 ],
60+
[ Infinity, Infinity, 0, 1 ],
61+
[ Infinity, Infinity, Infinity, 0 ]
62+
]
63+
expect(johnson(graph)).toStrictEqual(expected);
64+
});
65+
66+
it("should return the undefined for two node graph with negative-weight cycle", () => {
67+
let graph = init_graph(2);
68+
add_edge(graph, 0, 1, -1);
69+
expect(johnson(graph)).toStrictEqual(undefined);
70+
});
71+
72+
it("should return the undefined for three node graph with negative-weight cycle", () => {
73+
let graph = init_graph(3);
74+
graph[0].push([1, -1]);
75+
graph[0].push([2, 7]);
76+
graph[1].push([2, -5]);
77+
graph[2].push([0, 4]);
78+
expect(johnson(graph)).toStrictEqual(undefined);
79+
});
80+
81+
it("should return the correct value for zero element graph", () => {
82+
expect(johnson([])).toStrictEqual([]);
83+
});
84+
85+
it("should return the correct value for single element graph", () => {
86+
expect(johnson([[]])).toStrictEqual([[0]]);
87+
});
88+
89+
it("should return the correct value for a linear graph", () => {
90+
let linear_graph = init_graph(4);
91+
add_edge(linear_graph, 0, 1, 1);
92+
add_edge(linear_graph, 1, 2, 2);
93+
add_edge(linear_graph, 2, 3, 3);
94+
95+
let expected = [[0, 1, 3, 6 ], [1, 0, 2, 5], [3, 2, 0, 3], [6, 5, 3, 0]];
96+
expect(johnson(linear_graph)).toStrictEqual(expected);
97+
});
98+
99+
it("should return the correct value for a linear graph with unreachable node", () => {
100+
let linear_graph = init_graph(3);
101+
add_edge(linear_graph, 0, 1, 1);
102+
103+
let expected = [[0, 1, Infinity], [1, 0, Infinity], [Infinity, Infinity, 0]];
104+
expect(johnson(linear_graph)).toStrictEqual(expected);
105+
});
106+
})
107+

0 commit comments

Comments
 (0)