diff --git a/lib/alg/bellman-ford.js b/lib/alg/bellman-ford.js new file mode 100644 index 00000000..886e6cb9 --- /dev/null +++ b/lib/alg/bellman-ford.js @@ -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; +} diff --git a/lib/alg/index.js b/lib/alg/index.js index 2c3d76f2..939f66fc 100644 --- a/lib/alg/index.js +++ b/lib/alg/index.js @@ -1,4 +1,5 @@ module.exports = { + bellmanFord: require("./bellman-ford"), components: require("./components"), dijkstra: require("./dijkstra"), dijkstraAll: require("./dijkstra-all"), diff --git a/test/alg/bellman-ford-tests.js b/test/alg/bellman-ford-tests.js new file mode 100644 index 00000000..2a265690 --- /dev/null +++ b/test/alg/bellman-ford-tests.js @@ -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); + }; +}