Skip to content

Commit

Permalink
Implement the Bellman-Ford algorithm and add tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
mstou committed Apr 29, 2020
1 parent 65489f3 commit 9fe70eb
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 0 deletions.
75 changes: 75 additions & 0 deletions lib/alg/bellman-ford.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
var _ = require("../lodash");

module.exports = bellmanFord;

var DEFAULT_WEIGHT_FUNC = _.constant(1);

function bellmanFord(g, source, weightFn, edgeFn) {
return runBellmanFord(
g,
String(source),
weightFn || DEFAULT_WEIGHT_FUNC,
edgeFn || function(v) { return g.outEdges(v); }
);
}


function runBellmanFord(g, source, weightFn, edgeFn) {
var results = {},
didADistanceUpgrade = true,
iterations = 0,
nodes = g.nodes();

var relaxEdge = function(edge) {
var edgeWeight = weightFn(edge);
if( results[edge.v].distance + edgeWeight < results[edge.w].distance ){
results[edge.w] = {
distance: results[edge.v].distance + edgeWeight,
predecessor: edge.v
};
didADistanceUpgrade = true;
}
};

var relaxAllEdges = function() {
nodes.forEach(function(vertex) {
edgeFn(vertex).forEach(function(edge) {
// If the vertex on which the edgeFun in called is
// the edge.w, then we treat the edge as if it was reversed
var inVertex = edge.v === vertex ? edge.v : edge.w;
var outVertex = inVertex === edge.v ? edge.w : edge.v;
relaxEdge({ v: inVertex, w: outVertex });
});
});
};

// Initialization
nodes.forEach(function(v) {
var distance = v === source ? 0 : Number.POSITIVE_INFINITY;
results[v] = { distance: distance };
});

var numberOfNodes = nodes.length;

// Relax all edges in |V|-1 iterations
for(var i = 1; i < numberOfNodes; i++){
didADistanceUpgrade = false;
iterations++;
relaxAllEdges();
if (!didADistanceUpgrade) {
// Ιf no update was made in an iteration, Bellman-Ford has finished
break;
}
}

// Detect if the graph contains a negative weight cycle
if (iterations === numberOfNodes - 1) {
didADistanceUpgrade = false;
relaxAllEdges();
if (didADistanceUpgrade) {
throw new Error("The graph contains a negative weight cycle");
}
}

return results;
}
1 change: 1 addition & 0 deletions lib/alg/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
bellmanFord: require("./bellman-ford"),
components: require("./components"),
dijkstra: require("./dijkstra"),
dijkstraAll: require("./dijkstra-all"),
Expand Down
121 changes: 121 additions & 0 deletions test/alg/bellman-ford-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
var expect = require("../chai").expect;

var Graph = require("../..").Graph,
bellmanFord = require("../..").alg.bellmanFord;

describe("alg.bellmanFord", function(){
it("Assigns distance 0 for the source node", function() {
var g = new Graph();
g.setNode("source");
expect(bellmanFord(g, "source")).to.eql({ source: { distance: 0 } });
});

it("Returns Number.POSITIVE_INFINITY for unconnected nodes", function() {
var g = new Graph();
g.setNode("a");
g.setNode("b");
expect(bellmanFord(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: Number.POSITIVE_INFINITY }
});
});

it("Returns the distance and predecessor for all nodes", function() {
var g = new Graph();
g.setPath(["a", "b", "c"]);
g.setEdge("b", "d");
expect(bellmanFord(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: 1, predecessor: "a" },
c: { distance: 2, predecessor: "b" },
d: { distance: 2, predecessor: "b" }
});
});

it("Works for undirected graphs", function() {
var g = new Graph({ directed: false });
g.setPath(["a", "b", "c"]);
g.setEdge("b", "d");
expect(bellmanFord(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: 1, predecessor: "a" },
c: { distance: 2, predecessor: "b" },
d: { distance: 2, predecessor: "b" }
});
});

it("Works with an optionally supplied weight function", function() {
var g = new Graph();
g.setEdge("a", "b", 1);
g.setEdge("a", "c", 2);
g.setEdge("b", "d", 3);
g.setEdge("c", "d", 3);

expect(bellmanFord(g, "a", weightFn(g))).to.eql({
a: { distance: 0 },
b: { distance: 1, predecessor: "a" },
c: { distance: 2, predecessor: "a" },
d: { distance: 4, predecessor: "b" }
});
});

it("Uses an optionally supplied edge function", function() {
var g = new Graph();
g.setPath(["a", "c", "d"]);
g.setEdge("b", "c");

function edgeFn(v){
switch (v) {
case "d": return [{ v: "d", w: "c" }];
case "c": return [{ v: "c", w: "b" }, { v: "c", w: "a" }];
default: return [];
}
}

expect(bellmanFord(g, "d", undefined, edgeFn)).to.eql({
a: { distance: 2, predecessor: "c" },
b: { distance: 2, predecessor: "c" },
c: { distance: 1, predecessor: "d" },
d: { distance: 0 }
});
});

it("Works with negative weight edges on the graph", function() {
var g = new Graph();
g.setEdge("a", "b", -1);
g.setEdge("a", "c", 4);
g.setEdge("b", "c", 3);
g.setEdge("b", "d", 2);
g.setEdge("b", "e", 2);
g.setEdge("d", "c", 5);
g.setEdge("d", "b", 1);
g.setEdge("e", "d", -3);

expect(bellmanFord(g, "a", weightFn(g))).to.eql({
a: { distance: 0 },
b: { distance: -1, predecessor: "a" },
c: { distance: 2, predecessor: "b" },
d: { distance: -2, predecessor: "e" },
e: { distance: 1, predecessor: "b" }
});
});

it("Throws an error if the graph contains a negative weight cycle", function() {
var g = new Graph();
g.setEdge("a", "b", 1);
g.setEdge("b", "c", 3);
g.setEdge("c", "d", -5);
g.setEdge("d", "e", 4);
g.setEdge("d", "b", 1);
g.setEdge("c", "f", 8);

expect(function() { bellmanFord(g, "a", weightFn(g)); } ).to.throw();
});
});


function weightFn(g) {
return function(e) {
return g.edge(e);
};
}

0 comments on commit 9fe70eb

Please sign in to comment.