Skip to content
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

Add Bellman-Ford algorithm #88

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we switch some of the 'var' declarations in this PR to let?

if( results[edge.v].distance + edgeWeight < results[edge.w].distance ){
mstou marked this conversation as resolved.
Show resolved Hide resolved
mstou marked this conversation as resolved.
Show resolved Hide resolved
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;
}
2 changes: 2 additions & 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 All @@ -8,6 +9,7 @@ module.exports = {
postorder: require("./postorder"),
preorder: require("./preorder"),
prim: require("./prim"),
shortestPaths: require("./shortest-paths"),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks good that you extract it but the documentation will need to mention that this API can run bellman ford or Dijkstra

tarjan: require("./tarjan"),
topsort: require("./topsort")
};
42 changes: 42 additions & 0 deletions lib/alg/shortest-paths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
var dijkstra = require("./dijkstra"),
bellmanFord = require("./bellman-ford");

module.exports = shortestPaths;

function shortestPaths(g, source, weightFn, edgeFn){
return runShortestPaths(
g,
source,
weightFn,
edgeFn || function(v) { return g.outEdges(v); }
);
}

function runShortestPaths(g, source, weightFn, edgeFn) {
if (weightFn === undefined) {
return dijkstra(g, source, weightFn, edgeFn);
}

var negativeEdgeExists = false;
var nodes = g.nodes();

for (var i = 0; i < nodes.length; i++) {
var adjList = edgeFn(nodes[i]);

for (var j = 0; j < adjList.length; j++) {
var edge = adjList[j];
var inVertex = edge.v === nodes[i] ? edge.v : edge.w;
var outVertex = inVertex === edge.v ? edge.w : edge.v;

if (weightFn({ v: inVertex, w: outVertex }) < 0) {
negativeEdgeExists = true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can break in case a negative edge was found

}
}

if (negativeEdgeExists) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can extract this if outside the scope and just write:
return negativeEdgeExists? bellmanFord(g, source, weightFn, edgeFn): dijkstra(g, source, weightFn, edgeFn)

return bellmanFord(g, source, weightFn, edgeFn);
}
}

return dijkstra(g, source, weightFn, edgeFn);
}
47 changes: 47 additions & 0 deletions test/alg/bellman-ford-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
var expect = require("../chai").expect,
Graph = require("../..").Graph,
bellmanFord = require("../..").alg.bellmanFord,
shortestPathsTests = require("./shortest-paths-tests.js");

describe("alg.bellmanFord", function(){
shortestPathsTests(bellmanFord);

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);
};
}
76 changes: 5 additions & 71 deletions test/alg/dijkstra-test.js
Original file line number Diff line number Diff line change
@@ -1,76 +1,10 @@
var expect = require("../chai").expect;

var Graph = require("../..").Graph;
var dijkstra = require("../..").alg.dijkstra;
var expect = require("../chai").expect,
Graph = require("../..").Graph,
dijkstra = require("../..").alg.dijkstra,
shortestPathsTests = require("./shortest-paths-tests.js");

describe("alg.dijkstra", function() {
it("assigns distance 0 for the source node", function() {
var g = new Graph();
g.setNode("source");
expect(dijkstra(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(dijkstra(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: Number.POSITIVE_INFINITY }
});
});

it("returns the distance and path from the source node to other nodes", function() {
var g = new Graph();
g.setPath(["a", "b", "c"]);
g.setEdge("b", "d");
expect(dijkstra(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(dijkstra(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: 1, predecessor: "a" },
c: { distance: 2, predecessor: "b" },
d: { distance: 2, predecessor: "b" }
});
});

it("uses 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(dijkstra(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");

expect(dijkstra(g, "d", undefined, function(e) { return g.inEdges(e); })).to.eql({
a: { distance: 2, predecessor: "c" },
b: { distance: 2, predecessor: "c" },
c: { distance: 1, predecessor: "d" },
d: { distance: 0 }
});
});
shortestPathsTests(dijkstra);

it("throws an Error if it encounters a negative edge weight", function() {
var g = new Graph();
Expand Down
106 changes: 106 additions & 0 deletions test/alg/shortest-paths-tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
var expect = require("../chai").expect,
Graph = require("../..").Graph,
alg = require("../..").alg;

module.exports = tests;

function tests(algorithm) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe adding a comment that this reflect for bellman ford or Dijkstra?

describe( "Shortest Path Algorithms", function() {
it("assigns distance 0 for the source node", function() {
var g = new Graph();
g.setNode("source");
expect(algorithm(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(algorithm(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: Number.POSITIVE_INFINITY }
});
});

it("returns the distance and path from the source node to other nodes", function() {
var g = new Graph();
g.setPath(["a", "b", "c"]);
g.setEdge("b", "d");
expect(algorithm(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(algorithm(g, "a")).to.eql({
a: { distance: 0 },
b: { distance: 1, predecessor: "a" },
c: { distance: 2, predecessor: "b" },
d: { distance: 2, predecessor: "b" }
});
});

it("uses 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(algorithm(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");

expect(algorithm(g, "d", undefined, function(e) { return g.inEdges(e); })).to.eql({
a: { distance: 2, predecessor: "c" },
b: { distance: 2, predecessor: "c" },
c: { distance: 1, predecessor: "d" },
d: { distance: 0 }
});
});
});
}

// Test shortestPaths() function
describe("alg.shortestPaths", function() {
tests(alg.shortestPaths);

it("uses dijkstra if no weightFn is provided", function() {
var g = new Graph();
g.setPath(["a", "b", "c"]);
g.setEdge("b", "d", -10);

expect(alg.shortestPaths(g, "a")).to.eql(alg.dijkstra(g, "a"));
});

it("uses bellman-ford if the graph contains a negative edge", function() {
var g = new Graph();
g.setEdge("a", "b", 10);
g.setEdge("b", "c", 8);
g.setEdge("a", "d", -3);
g.setEdge("d", "c", 2);

expect(alg.shortestPaths(g, "a", weightFn(g))).to.eql(alg.bellmanFord(g, "a", weightFn(g)));
});
});

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