From 6a18e98d050b15909eedb0091b20b345a0f11ddb Mon Sep 17 00:00:00 2001 From: Basil Peace Date: Tue, 27 Aug 2019 20:52:32 +0300 Subject: [PATCH] feat: add least common ancestors algorithm --- README.md | 9 +++++++++ index.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- test.js | 22 ++++++++++++++++++++-- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 27f7498..d994507 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,15 @@ Arguments: * *sourceNodes* (optional) - An array of node identifier strings. This specifies the subset of nodes to use as the sources of the depth-first search. If *sourceNodes* is not specified, all **[nodes](#nodes)** in the graph are used as source nodes. * *includeSourceNodes* (optional) - A boolean specifying whether or not to include the source nodes in the returned array. If *includeSourceNodes* is not specified, it is treated as `true` (all source nodes are included in the returned array). +# graph.lowestCommonAncestors([node1][, node2]) + +Performs search of [Lowest common ancestors](https://en.wikipedia.org/wiki/Lowest_common_ancestor). Returns an array of node identifier strings. + +Arguments: + + * *node1* (required) - First node. + * *node2* (required) - Second node. + # graph.topologicalSort([sourceNodes][, includeSourceNodes]) Performs [Topological Sort](https://en.wikipedia.org/wiki/Topological_sorting). Returns an array of node identifier strings. The returned array includes nodes in topologically sorted order. This means that for each visited edge (**u** -> **v**), **u** comes before **v** in the topologically sorted order. Amazingly, this comes from simply reversing the result from depth first search. Inspired by by Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 613. diff --git a/index.js b/index.js index 0e3836d..3bc9911 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ module.exports = function Graph(serialized){ indegree: indegree, outdegree: outdegree, depthFirstSearch: depthFirstSearch, + lowestCommonAncestors: lowestCommonAncestors, topologicalSort: topologicalSort, shortestPath: shortestPath, serialize: serialize, @@ -46,7 +47,7 @@ module.exports = function Graph(serialized){ // Removes a node from the graph. // Also removes incoming and outgoing edges. function removeNode(node){ - + // Remove incoming edges. Object.keys(edges).forEach(function (u){ edges[u].forEach(function (v){ @@ -147,7 +148,7 @@ module.exports = function Graph(serialized){ // Depth First Search algorithm, inspired by // Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 604 - // This variant includes an additional option + // This variant includes an additional option // `includeSourceNodes` to specify whether to include or // exclude the source nodes from the result (true by default). // If `sourceNodes` is not specified, all nodes in the graph @@ -187,6 +188,50 @@ module.exports = function Graph(serialized){ return nodeList; } + // Least Common Ancestors + // Inspired by https://github.com/relaxedws/lca/blob/master/src/LowestCommonAncestor.php code + // but uses depth search instead of breadth. Also uses some optimizations + function lowestCommonAncestors(node1, node2){ + + var node1Ancestors = []; + var lcas = []; + + function CA1Visit(visited, node){ + if(!visited[node]){ + visited[node] = true; + node1Ancestors.push(node); + if (node == node2) { + lcas.push(node); + return false; // found - shortcut + } + return adjacent(node).every(node => { + return CA1Visit(visited, node); + }); + } else { + return true; + } + } + + function CA2Visit(visited, node){ + if(!visited[node]){ + visited[node] = true; + if (node1Ancestors.indexOf(node) >= 0) { + lcas.push(node); + } else if (lcas.length == 0) { + adjacent(node).forEach(node => { + CA2Visit(visited, node); + }); + } + } + } + + if (CA1Visit({}, node1)) { // No shortcut worked + CA2Visit({}, node2); + } + + return lcas; + } + // The topological sort algorithm yields a list of visited nodes // such that for each visited edge (u, v), u comes before v in the list. // Amazingly, this comes from just reversing the result from depth first search. @@ -326,6 +371,6 @@ module.exports = function Graph(serialized){ serialized.links.forEach(function (link){ addEdge(link.source, link.target, link.weight); }); return graph; } - + return graph; } diff --git a/test.js b/test.js index f41f3a2..befd3e5 100644 --- a/test.js +++ b/test.js @@ -198,8 +198,8 @@ describe("Graph", function() { graph.addEdge("a", "d"); // | d graph.addEdge("b", "c"); // c | graph.addEdge("d", "e"); // \ / - graph.addEdge("c", "e"); // e - + graph.addEdge("c", "e"); // e + var sorted = graph.topologicalSort(["a"], false); assert.equal(sorted.length, 4); assert(contains(sorted, "b")); @@ -245,6 +245,24 @@ describe("Graph", function() { output(graph, "cycles"); }); + + it("Should compute lowest common ancestors.", function (){ + var graph = Graph() + + .addEdge("a", "b") + .addEdge("b", "d") + .addEdge("c", "d") + .addEdge("b", "e") + .addEdge("c", "e") + .addEdge("d", "g") + .addEdge("e", "g") + .addNode("f"); + + assert.deepStrictEqual(graph.lowestCommonAncestors("a", "a"), ["a"]); + assert.deepStrictEqual(graph.lowestCommonAncestors("a", "b"), ["b"]); + assert.deepStrictEqual(graph.lowestCommonAncestors("a", "c"), ["d", "e"]); + assert.deepStrictEqual(graph.lowestCommonAncestors("a", "f"), []); + }); }); describe("Edge cases and error handling", function() {