From 5c1bc9e54f3f28a6450879e49bac18bdbda58481 Mon Sep 17 00:00:00 2001 From: eush77 Date: Sat, 19 Jul 2014 14:37:02 +0400 Subject: [PATCH 1/2] Add Kruskal's minimum spanning tree algorithm --- algorithms/graph/kruskal.js | 71 +++++++ .../algorithms/graph/minimum_spanning_tree.js | 201 ++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 algorithms/graph/kruskal.js create mode 100644 test/algorithms/graph/minimum_spanning_tree.js diff --git a/algorithms/graph/kruskal.js b/algorithms/graph/kruskal.js new file mode 100644 index 0000000..b637aed --- /dev/null +++ b/algorithms/graph/kruskal.js @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2014 Eugene Sharygin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +'use strict'; + +var DisjointSetForest = require('../../data_structures/disjoint_set_forest'), + Graph = require('../../data_structures/graph'); + + +/** + * Kruskal's minimum spanning tree (forest) algorithm. + * Complexity: O(E * log(V)). + * + * @param {Graph} graph - Undirected graph. + * @return {Graph} Minimum spanning tree or forest + * (depending on whether input graph is connected itself). + */ +var kruskal = function (graph) { + if (graph.directed) { + throw new Error('Can\'t build MST of a directed graph.'); + } + + var connectedComponents = new DisjointSetForest(); + var mst = new Graph(false); + graph.vertices.forEach(mst.addVertex.bind(mst)); + + var edges = graph.vertices.reduce(function (edges, vertex) { + graph.neighbors(vertex).forEach(function (neighbor) { + // Compared as strings, loops intentionally omitted. + if (vertex < neighbor) { + edges.push({ + ends: [vertex, neighbor], + weight: graph.edge(vertex, neighbor) + }); + } + }); + return edges; + }, []); + + edges.sort(function (a, b) { + return a.weight - b.weight; + }).forEach(function (edge) { + if (!connectedComponents.sameSubset(edge.ends[0], edge.ends[1])) { + mst.addEdge(edge.ends[0], edge.ends[1], edge.weight); + connectedComponents.merge(edge.ends[0], edge.ends[1]); + } + }); + + return mst; +}; + + +module.exports = kruskal; diff --git a/test/algorithms/graph/minimum_spanning_tree.js b/test/algorithms/graph/minimum_spanning_tree.js new file mode 100644 index 0000000..8db2383 --- /dev/null +++ b/test/algorithms/graph/minimum_spanning_tree.js @@ -0,0 +1,201 @@ +/** + * Copyright (C) 2014 Eugene Sharygin + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"], to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +'use strict'; + +var kruskal = require('../../../algorithms/graph/kruskal'), + Graph = require('../../../data_structures/graph'), + assert = require('assert'); + + +/** + * @param {Graph} graph - Undirected graph. + * @return {number} + */ +var numberOfConnectedComponents = function (graph) { + assert(!graph.directed); + var seen = {}; + var coverComponent = function dfs(start) { + if (!seen[start]) { + seen[start] = true; + graph.neighbors(start).forEach(dfs); + } + }; + return graph.vertices.reduce(function (count, vertex) { + if (seen[vertex]) { + return count; + } + else { + coverComponent(vertex); + return count + 1; + } + }, 0); +}; + + +/** + * Test whether graph is a valid (undirected) forest. + * In a forest #vertices = #edges + #components. + * + * @param {Graph} graph + * @param {number} connectivity + * @return {boolean} + */ +var isForest = function (graph, connectivity) { + if (graph.directed || numberOfConnectedComponents(graph) != connectivity) { + return false; + } + var numberOfEdges = graph.vertices.reduce(function (numberOfEdges, vertex) { + return numberOfEdges + graph.neighbors(vertex).filter(function (neighbor) { + return vertex <= neighbor; + }).length; + }, 0); + return graph.vertices.length == numberOfEdges + connectivity; +}; + + +/** + * Test whether two graphs share the same vertex set. + * + * @param {Graph} graph1 + * @param {Graph} graph2 + * @return {boolean} + */ +var spans = function (graph1, graph2) { + var str = function (graph) { + return JSON.stringify(graph.vertices.sort()); + }; + return str(graph1) == str(graph2); +}; + + +/** + * Sum up graph edge weights. + * + * @param {Graph} graph + * @return {?number} Null if the graph contains no edges. + */ +var graphCost = function (graph) { + var noEdges = true; + var total = graph.vertices.reduce(function (cost, vertex) { + return cost + graph.neighbors(vertex).reduce(function (cost, neighbor) { + noEdges = false; + return cost + graph.edge(vertex, neighbor); + }, 0); + }, 0); + return noEdges ? null : graph.directed ? total : total / 2; +}; + + +/** + * Test whether one graph is the minimum spanning forest of the other. + * + * @param {Graph} suspect + * @param {Graph} graph - Undirected graph. + * @param {?number} minimumCost + * @param {number} [connectivity=1] + * @return {boolean} + */ +var isMinimumSpanningForest = function (suspect, graph, + minimumCost, connectivity) { + assert(!graph.directed); + return isForest(suspect, connectivity || 1) && + spans(suspect, graph) && + graphCost(suspect) === minimumCost; +}; + + +var testMstAlgorithm = function (mst) { + it('should find a minimum spanning tree', function () { + var graph = new Graph(false); + graph.addEdge(1, 2, 1); + graph.addEdge(1, 4, 2); + graph.addEdge(1, 5, 2); + graph.addEdge(4, 2, 2); + graph.addEdge(4, 5, 1); + graph.addEdge(5, 6, 1); + graph.addEdge(6, 4, 8); + graph.addEdge(6, 3, 2); + graph.addEdge(2, 3, 3); + assert(isMinimumSpanningForest(mst(graph), graph, 7)); + + // Change (4, 5) weight to 3. + graph.addEdge(4, 5, +2); + assert(isMinimumSpanningForest(mst(graph), graph, 8)); + + // Force it go find another way to reach (6). + graph.addEdge(4, 5, +5); + graph.addEdge(6, 5, +7); + assert(isMinimumSpanningForest(mst(graph), graph, 10)); + + // It should find zero-cost MST. + var clear = function (a, b) { + graph.addEdge(a, b, -graph.edge(a, b)); + }; + clear(2, 1); + clear(1, 5); + clear(5, 6); + clear(6, 4); + clear(6, 3); + assert(isMinimumSpanningForest(mst(graph), graph, 0)); + + // But prefer a negative-cost one to it. + graph.addEdge(2, 4, -102); + assert(isMinimumSpanningForest(mst(graph), graph, -100)); + }); + + it('should find a minimum spaning forest if the graph is not connected', + function () { + var graph = new Graph(false); + graph.addVertex(1); + graph.addVertex(2); + graph.addVertex(3); + assert(isMinimumSpanningForest(mst(graph), graph, null, 3)); + + graph.addEdge(1, 2, 2); + assert(isMinimumSpanningForest(mst(graph), graph, 2, 2)); + + graph.addEdge(1, 3, 1); + graph.addEdge(2, 3, -1); + assert(isMinimumSpanningForest(mst(graph), graph, 0, 1)); + + graph.addVertex(4); + assert(isMinimumSpanningForest(mst(graph), graph, 0, 2)); + + graph.addEdge(5, 6, 1); + assert(isMinimumSpanningForest(mst(graph), graph, 1, 3)); + + graph.addEdge(5, 4, -100); + graph.addEdge(6, 4, -100); + assert(isMinimumSpanningForest(mst(graph), graph, -200, 2)); + }); + + it('should throw an error if the graph is directed', function () { + var directedGraph = new Graph(true); + directedGraph.addEdge('Rock', 'Hard Place'); + assert.throws(mst.bind(null, directedGraph)); + }); +}; + + +describe('Minimum Spanning Tree', function () { + describe('#Kruskal\'s Algorithm', testMstAlgorithm.bind(null, kruskal)); +}); From 259d3efb96ee14183ace35dfc680bf947945151b Mon Sep 17 00:00:00 2001 From: eush77 Date: Tue, 22 Jul 2014 13:45:06 +0400 Subject: [PATCH 2/2] Include Kruskal's algorithm in main.js --- main.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/main.js b/main.js index ad27ecd..45fe369 100644 --- a/main.js +++ b/main.js @@ -28,7 +28,8 @@ var lib = { SPFA: require('./algorithms/graph/SPFA'), bellmanFord: require('./algorithms/graph/bellman_ford'), eulerPath: require('./algorithms/graph/euler_path'), - depthFirstSearch: require('./algorithms/graph/depth_first_search') + depthFirstSearch: require('./algorithms/graph/depth_first_search'), + kruskal: require('./algorithms/graph/kruskal'), }, Math: { fibonacci: require('./algorithms/math/fibonacci'),