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() {