diff --git a/lib/alg/bellman-ford.js b/lib/alg/bellman-ford.js new file mode 100644 index 00000000..3e4afff8 --- /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..0dcd0251 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"), @@ -8,6 +9,7 @@ module.exports = { postorder: require("./postorder"), preorder: require("./preorder"), prim: require("./prim"), + shortestPaths: require("./shortest-paths"), tarjan: require("./tarjan"), topsort: require("./topsort") }; diff --git a/lib/alg/shortest-paths.js b/lib/alg/shortest-paths.js new file mode 100644 index 00000000..a1453611 --- /dev/null +++ b/lib/alg/shortest-paths.js @@ -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; + } + } + + if (negativeEdgeExists) { + return bellmanFord(g, source, weightFn, edgeFn); + } + } + + return dijkstra(g, source, weightFn, edgeFn); +} diff --git a/test/alg/bellman-ford-tests.js b/test/alg/bellman-ford-tests.js new file mode 100644 index 00000000..b2857dca --- /dev/null +++ b/test/alg/bellman-ford-tests.js @@ -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); + }; +} diff --git a/test/alg/dijkstra-test.js b/test/alg/dijkstra-test.js index bfb77737..126f7349 100644 --- a/test/alg/dijkstra-test.js +++ b/test/alg/dijkstra-test.js @@ -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(); diff --git a/test/alg/shortest-paths-tests.js b/test/alg/shortest-paths-tests.js new file mode 100644 index 00000000..cfc2dde2 --- /dev/null +++ b/test/alg/shortest-paths-tests.js @@ -0,0 +1,106 @@ +var expect = require("../chai").expect, + Graph = require("../..").Graph, + alg = require("../..").alg; + +module.exports = tests; + +function tests(algorithm) { + 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); + }; +}