Skip to content

feat: add bellman-ford algorithm #136

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 1 commit into from
Jun 12, 2023
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
45 changes: 45 additions & 0 deletions graph/bellman_ford.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* @function bellmanFord
* @description Compute the shortest path from a source node to all other nodes. If there is negative weight cycle, returns undefined. 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.
* @Complexity_Analysis
* Time complexity: O(E*V)
* Space Complexity: O(V)
* @param {[number, number][][]} graph - The graph in adjacency list form
* @param {number} start - The source node
* @return {number[] | undefined} - The shortest path to each node, undefined if there is negative weight cycle
* @see https://en.wikipedia.org/wiki/Bellman%E2%80%93Ford_algorithm
*/
export const bellmanFord = (graph: [number, number][][], start: number): number[] | undefined => {
// We save the shortest distance to each node in `distances`. If a node is
// unreachable from the start node, its distance is Infinity.
let distances = Array(graph.length).fill(Infinity);
distances[start] = 0;

// On the i'th iteration, we compute all shortest paths that consists of i+1
// nodes. If we compute this V-1 times, we will have computed all simple
// shortest paths in the graph because a shortest path has at most V nodes.
for (let i = 0; i < graph.length - 1; ++i) {
for (let node = 0; node < graph.length; ++node) {
for (const [child, weight] of graph[node]) {
const new_distance = distances[node] + weight;
if (new_distance < distances[child]) {
distances[child] = new_distance
}
}
}
}

// Look through all edges. If the shortest path to a destination node d is
// larger than the distance to source node s and weight(s->d), then the path
// to s must have a negative weight cycle.
for (let node = 0; node < graph.length; ++node) {
for (const [child, weight] of graph[node]) {
if (distances[child] > distances[node] + weight) {
return undefined;
}
}
}

return distances;
}

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

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

describe("bellmanFord", () => {

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);
expect(bellmanFord(graph, 0)).toStrictEqual([0, 4, 12, 19, 21, 11, 9, 8, 14]);
});

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

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);
test.each([[0, [0, 1, 3, 6]], [1, [1, 0, 2, 5]], [2, [3, 2, 0, 3]], [3, [6, 5, 3, 0]]])(
"correct result for linear graph with source node %i",
(source, result) => {
expect(bellmanFord(linear_graph, source)).toStrictEqual(result);
}
);

let unreachable_graph = init_graph(3);
add_edge(unreachable_graph, 0, 1, 1);
test.each([[0, [0, 1, Infinity]], [1, [1, 0, Infinity]], [2, [Infinity, Infinity, 0]]])(
"correct result for graph with unreachable nodes with source node %i",
(source, result) => {
expect(bellmanFord(unreachable_graph, source)).toStrictEqual(result);
}
);
})

describe("bellmanFord negative cycle graphs", () => {
it("should returned undefined for 2-node graph with negative cycle", () => {
let basic = init_graph(2);
basic[0].push([1, 2]);
basic[1].push([0, -3]);
expect(bellmanFord(basic, 0)).toStrictEqual(undefined);
expect(bellmanFord(basic, 1)).toStrictEqual(undefined);
});

it("should returned undefined for graph with negative cycle", () => {
let negative = init_graph(5);
negative[0].push([1, 6]);
negative[0].push([3, 7]);
negative[1].push([2, 5]);
negative[1].push([3, 8]);
negative[1].push([4, -4]);
negative[2].push([1, -4]);
negative[3].push([2, -3]);
negative[3].push([4, 9]);
negative[4].push([0, 3]);
negative[4].push([2, 7]);
for (let i = 0; i < 5; ++i) {
expect(bellmanFord(negative, i)).toStrictEqual(undefined);
}
});
});