-1

Note that I've already gone through How to detect a loop in a hierarchy of javascript elements In our case, we're not dealing with a linked-list, but a hierarchical graph where each node may have multiple children linked to it. For example,

const graph = {
   a: {value: Va, children: {b, d}},
   b: {value: Vb, children: {c}},
   c: {value: Vc, children: {a, d, e}}
}

In this graph, we should detect the loop a -> b -> c -> a.

1man
  • 5,216
  • 7
  • 42
  • 56
  • So where is your problem? Please show us the code, and explain [which algorithm](https://en.wikipedia.org/wiki/Cycle_detection) you tried to implement. – Bergi Feb 06 '21 at 06:04
  • @Bergi This is a quote from the Wikipedia article you linked to: "One can view the same problem graph-theoretically, by constructing a functional graph (that is, a directed graph in which each vertex has a single outgoing edge) the vertices of which are the elements of S and the edges of which map an element to the corresponding function value." As I explained in the question, we're dealing with a directed graph in which each vertex has a MULTIPLE outgoing edges. – 1man Feb 06 '21 at 13:28
  • That quote refers to "**constructing** a functional graph from the input graph" - it doesn't limit the shape of the input graph at all. – Bergi Feb 06 '21 at 14:07

2 Answers2

2

If you only need to determine whether a graph has a cycle, and don't need to report what the cycle is, you can do this for a particular node of the graph with a simple recursion and wrap that in a call that tests all nodes.

Here's one implementation:

const hasCycle = (graph, name, path = []) =>
  path .includes (name)
    ? true
   : (graph?.[name]?.children ?? []) .some (c => hasCycle (graph, c, [...path, name]))

const anyCycles = (graph) =>
  Object .keys (graph) .some (k => hasCycle (graph, k))

const graph1 = {a: {value: 1, children: ['b', 'd']}, b: {value: 2, children: ['c']}, c: {value: 3, children: ['a', 'd', 'e']}}
const graph2 = {a: {value: 1, children: ['b', 'd']}, b: {value: 2, children: ['c']}, c: {value: 3, children: ['d', 'e']}}

console .log (anyCycles (graph1))
console .log (anyCycles (graph2))
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    I wish I could mark both answers as correct. I ended up using both answers in different parts of the program. Thank you. – 1man Feb 12 '21 at 13:39
1

There are some syntax errors in the graph object you shared, but assuming that the children are identified by strings, you would typically use a depth first traversal to find out if you bump into an edge that is a back reference to a node that was already on the current path.

If that happens you have a cycle, and the cycle can be easily derived from the current path and the back-referenced node.

To save repetition of traversals, you would also keep track of nodes that have been visited (whether on the current path or not). There is no need to continue a search from a node that was already visited.

For marking nodes as visited, you could use a Set.

function findCycle(graph) {
    let visited = new Set;
    let result;
    
    // dfs set the result to a cycle when the given node was already on the current path.
    //    If not on the path, and also not visited, it is marked as such. It then 
    //    iterates the node's children and calls the function recursively.
    //    If any of those calls returns true, exit with true also
    function dfs(node, path) {
        if (path.has(node)) {
            result = [...path, node]; // convert to array (a Set maintains insertion order)
            result.splice(0, result.indexOf(node)); // remove part that precedes the cycle
            return true;
        }
        if (visited.has(node)) return;
        path.add(node);
        visited.add(node);
        if ((graph[node]?.children || []).some(child => dfs(child, path))) return path;
        // Backtrack
        path.delete(node);
        // No cycle found here: return undefined
    }
    
    // Perform a DFS traversal for each node (except nodes that get 
    //   visited in the process)
    for (let node in graph) {
        if (!visited.has(node) && dfs(node, new Set)) return result;
    }
}

// Your example graph (with corrections):
const graph = {
   a: {value: 1, children: ["b", "d"]},
   b: {value: 2, children: ["c"]},
   c: {value: 3, children: ["a", "d", "e"]}
};

// Find the cycle
console.log(findCycle(graph)); // ["a","b","c","a"]

// Break the cycle, and run again
graph.c.children.shift(); // drop the edge c->a
console.log(findCycle(graph)); // undefined (i.e. no cycle)
trincot
  • 317,000
  • 35
  • 244
  • 286