Skip to content

Commit 7fe1f2b

Browse files
authored
Merge pull request #22 from sourcemetadata/master
Add least common ancestors algorithm
2 parents 531622a + 6a18e98 commit 7fe1f2b

3 files changed

Lines changed: 77 additions & 5 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,15 @@ Arguments:
210210
* *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.
211211
* *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).
212212

213+
<a name="lca" href="#lca">#</a> <i>graph</i>.<b>lowestCommonAncestors</b>([<i>node1</i>][, <i>node2</i>])
214+
215+
Performs search of [Lowest common ancestors](https://en.wikipedia.org/wiki/Lowest_common_ancestor). Returns an array of node identifier strings.
216+
217+
Arguments:
218+
219+
* *node1* (required) - First node.
220+
* *node2* (required) - Second node.
221+
213222
<a name="topological-sort" href="#topological-sort">#</a> <i>graph</i>.<b>topologicalSort</b>([<i>sourceNodes</i>][, <i>includeSourceNodes</i>])
214223

215224
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.

index.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module.exports = function Graph(serialized){
1414
indegree: indegree,
1515
outdegree: outdegree,
1616
depthFirstSearch: depthFirstSearch,
17+
lowestCommonAncestors: lowestCommonAncestors,
1718
topologicalSort: topologicalSort,
1819
shortestPath: shortestPath,
1920
serialize: serialize,
@@ -46,7 +47,7 @@ module.exports = function Graph(serialized){
4647
// Removes a node from the graph.
4748
// Also removes incoming and outgoing edges.
4849
function removeNode(node){
49-
50+
5051
// Remove incoming edges.
5152
Object.keys(edges).forEach(function (u){
5253
edges[u].forEach(function (v){
@@ -147,7 +148,7 @@ module.exports = function Graph(serialized){
147148

148149
// Depth First Search algorithm, inspired by
149150
// Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 604
150-
// This variant includes an additional option
151+
// This variant includes an additional option
151152
// `includeSourceNodes` to specify whether to include or
152153
// exclude the source nodes from the result (true by default).
153154
// If `sourceNodes` is not specified, all nodes in the graph
@@ -187,6 +188,50 @@ module.exports = function Graph(serialized){
187188
return nodeList;
188189
}
189190

191+
// Least Common Ancestors
192+
// Inspired by https://github.com/relaxedws/lca/blob/master/src/LowestCommonAncestor.php code
193+
// but uses depth search instead of breadth. Also uses some optimizations
194+
function lowestCommonAncestors(node1, node2){
195+
196+
var node1Ancestors = [];
197+
var lcas = [];
198+
199+
function CA1Visit(visited, node){
200+
if(!visited[node]){
201+
visited[node] = true;
202+
node1Ancestors.push(node);
203+
if (node == node2) {
204+
lcas.push(node);
205+
return false; // found - shortcut
206+
}
207+
return adjacent(node).every(node => {
208+
return CA1Visit(visited, node);
209+
});
210+
} else {
211+
return true;
212+
}
213+
}
214+
215+
function CA2Visit(visited, node){
216+
if(!visited[node]){
217+
visited[node] = true;
218+
if (node1Ancestors.indexOf(node) >= 0) {
219+
lcas.push(node);
220+
} else if (lcas.length == 0) {
221+
adjacent(node).forEach(node => {
222+
CA2Visit(visited, node);
223+
});
224+
}
225+
}
226+
}
227+
228+
if (CA1Visit({}, node1)) { // No shortcut worked
229+
CA2Visit({}, node2);
230+
}
231+
232+
return lcas;
233+
}
234+
190235
// The topological sort algorithm yields a list of visited nodes
191236
// such that for each visited edge (u, v), u comes before v in the list.
192237
// Amazingly, this comes from just reversing the result from depth first search.
@@ -326,6 +371,6 @@ module.exports = function Graph(serialized){
326371
serialized.links.forEach(function (link){ addEdge(link.source, link.target, link.weight); });
327372
return graph;
328373
}
329-
374+
330375
return graph;
331376
}

test.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ describe("Graph", function() {
198198
graph.addEdge("a", "d"); // | d
199199
graph.addEdge("b", "c"); // c |
200200
graph.addEdge("d", "e"); // \ /
201-
graph.addEdge("c", "e"); // e
202-
201+
graph.addEdge("c", "e"); // e
202+
203203
var sorted = graph.topologicalSort(["a"], false);
204204
assert.equal(sorted.length, 4);
205205
assert(contains(sorted, "b"));
@@ -245,6 +245,24 @@ describe("Graph", function() {
245245

246246
output(graph, "cycles");
247247
});
248+
249+
it("Should compute lowest common ancestors.", function (){
250+
var graph = Graph()
251+
252+
.addEdge("a", "b")
253+
.addEdge("b", "d")
254+
.addEdge("c", "d")
255+
.addEdge("b", "e")
256+
.addEdge("c", "e")
257+
.addEdge("d", "g")
258+
.addEdge("e", "g")
259+
.addNode("f");
260+
261+
assert.deepStrictEqual(graph.lowestCommonAncestors("a", "a"), ["a"]);
262+
assert.deepStrictEqual(graph.lowestCommonAncestors("a", "b"), ["b"]);
263+
assert.deepStrictEqual(graph.lowestCommonAncestors("a", "c"), ["d", "e"]);
264+
assert.deepStrictEqual(graph.lowestCommonAncestors("a", "f"), []);
265+
});
248266
});
249267

250268
describe("Edge cases and error handling", function() {

0 commit comments

Comments
 (0)