From 9fe70eb8d19fa071623adf0a2440cbf3db47d36d Mon Sep 17 00:00:00 2001 From: Miltiadis Stouras Date: Fri, 24 Aug 2018 03:44:20 +0300 Subject: [PATCH 1/3] Implement the Bellman-Ford algorithm and add tests. --- lib/alg/bellman-ford.js | 75 ++++++++++++++++++++ lib/alg/index.js | 1 + test/alg/bellman-ford-tests.js | 121 +++++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+) create mode 100644 lib/alg/bellman-ford.js create mode 100644 test/alg/bellman-ford-tests.js 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); + }; +} From fcce9e68cb9228795014f6087e5ba027512f9348 Mon Sep 17 00:00:00 2001 From: Miltiadis Stouras Date: Sat, 25 Aug 2018 17:01:25 +0300 Subject: [PATCH 2/3] Create a function that runs the common tests of all shortest paths algorithms. --- test/alg/bellman-ford-tests.js | 84 ++------------------------------ test/alg/dijkstra-test.js | 76 ++--------------------------- test/alg/shortest-paths-tests.js | 82 +++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 150 deletions(-) create mode 100644 test/alg/shortest-paths-tests.js diff --git a/test/alg/bellman-ford-tests.js b/test/alg/bellman-ford-tests.js index 2a265690..5a950362 100644 --- a/test/alg/bellman-ford-tests.js +++ b/test/alg/bellman-ford-tests.js @@ -1,84 +1,10 @@ -var expect = require("../chai").expect; - -var Graph = require("../..").Graph, - bellmanFord = require("../..").alg.bellmanFord; +var expect = require("../chai").expect, + Graph = require("../..").Graph, + bellmanFord = require("../..").alg.bellmanFord, + shortestPathsTests = require("./shortest-paths-tests.js"); 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 } - }); - }); + shortestPathsTests(bellmanFord); it("Works with negative weight edges on the graph", function() { var g = new Graph(); diff --git a/test/alg/dijkstra-test.js b/test/alg/dijkstra-test.js index bfb77737..61546185 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..8cb11b7e --- /dev/null +++ b/test/alg/shortest-paths-tests.js @@ -0,0 +1,82 @@ +var expect = require("../chai").expect, + Graph = require("../..").Graph; + +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 } + }); + }); + }); +} + +function weightFn(g) { + return function(e) { + return g.edge(e); + }; +} From f4b8e9c66b7659cac788989ea2e97fe9346e5859 Mon Sep 17 00:00:00 2001 From: Miltiadis Stouras Date: Sun, 2 Sep 2018 16:30:37 +0300 Subject: [PATCH 3/3] Implement the shortestPaths function that chooses whether to use Dijkstra or Bellmanford. --- lib/alg/bellman-ford.js | 6 ++--- lib/alg/index.js | 1 + lib/alg/shortest-paths.js | 42 ++++++++++++++++++++++++++++++++ test/alg/bellman-ford-tests.js | 6 ++--- test/alg/dijkstra-test.js | 6 ++--- test/alg/shortest-paths-tests.js | 26 +++++++++++++++++++- 6 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 lib/alg/shortest-paths.js diff --git a/lib/alg/bellman-ford.js b/lib/alg/bellman-ford.js index 886e6cb9..3e4afff8 100644 --- a/lib/alg/bellman-ford.js +++ b/lib/alg/bellman-ford.js @@ -16,9 +16,9 @@ function bellmanFord(g, source, weightFn, edgeFn) { function runBellmanFord(g, source, weightFn, edgeFn) { var results = {}, - didADistanceUpgrade = true, - iterations = 0, - nodes = g.nodes(); + didADistanceUpgrade = true, + iterations = 0, + nodes = g.nodes(); var relaxEdge = function(edge) { var edgeWeight = weightFn(edge); diff --git a/lib/alg/index.js b/lib/alg/index.js index 939f66fc..0dcd0251 100644 --- a/lib/alg/index.js +++ b/lib/alg/index.js @@ -9,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 index 5a950362..b2857dca 100644 --- a/test/alg/bellman-ford-tests.js +++ b/test/alg/bellman-ford-tests.js @@ -1,7 +1,7 @@ var expect = require("../chai").expect, - Graph = require("../..").Graph, - bellmanFord = require("../..").alg.bellmanFord, - shortestPathsTests = require("./shortest-paths-tests.js"); + Graph = require("../..").Graph, + bellmanFord = require("../..").alg.bellmanFord, + shortestPathsTests = require("./shortest-paths-tests.js"); describe("alg.bellmanFord", function(){ shortestPathsTests(bellmanFord); diff --git a/test/alg/dijkstra-test.js b/test/alg/dijkstra-test.js index 61546185..126f7349 100644 --- a/test/alg/dijkstra-test.js +++ b/test/alg/dijkstra-test.js @@ -1,7 +1,7 @@ var expect = require("../chai").expect, - Graph = require("../..").Graph, - dijkstra = require("../..").alg.dijkstra, - shortestPathsTests = require("./shortest-paths-tests.js"); + Graph = require("../..").Graph, + dijkstra = require("../..").alg.dijkstra, + shortestPathsTests = require("./shortest-paths-tests.js"); describe("alg.dijkstra", function() { shortestPathsTests(dijkstra); diff --git a/test/alg/shortest-paths-tests.js b/test/alg/shortest-paths-tests.js index 8cb11b7e..cfc2dde2 100644 --- a/test/alg/shortest-paths-tests.js +++ b/test/alg/shortest-paths-tests.js @@ -1,5 +1,6 @@ var expect = require("../chai").expect, - Graph = require("../..").Graph; + Graph = require("../..").Graph, + alg = require("../..").alg; module.exports = tests; @@ -75,6 +76,29 @@ function tests(algorithm) { }); } +// 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);