Skip to content

Commit

Permalink
Merge pull request #71 from eush77/kruskal
Browse files Browse the repository at this point in the history
Add Kruskal's minimum spanning tree algorithm
  • Loading branch information
felipernb committed Jul 22, 2014
2 parents 58e1d63 + 259d3ef commit 24aad9e
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 1 deletion.
71 changes: 71 additions & 0 deletions algorithms/graph/kruskal.js
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
201 changes: 201 additions & 0 deletions test/algorithms/graph/minimum_spanning_tree.js
Original file line number Diff line number Diff line change
@@ -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));
});

0 comments on commit 24aad9e

Please sign in to comment.