Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<a name="lca" href="#lca">#</a> <i>graph</i>.<b>lowestCommonAncestors</b>([<i>node1</i>][, <i>node2</i>])

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.

<a name="topological-sort" href="#topological-sort">#</a> <i>graph</i>.<b>topologicalSort</b>([<i>sourceNodes</i>][, <i>includeSourceNodes</i>])

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.
Expand Down
51 changes: 48 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = function Graph(serialized){
indegree: indegree,
outdegree: outdegree,
depthFirstSearch: depthFirstSearch,
lowestCommonAncestors: lowestCommonAncestors,
topologicalSort: topologicalSort,
shortestPath: shortestPath,
serialize: serialize,
Expand Down Expand Up @@ -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){
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
}
22 changes: 20 additions & 2 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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() {
Expand Down