0

This is a different question from what I have found so far related to cycles in graph as they relate to finding cycles in general but I need to find a cycle in a specific situation.

Consider the following graph:

n1: [n2]
n2: [n3]
n3: [n4]
n4: [n5, n2]
n5: []

This function should only return true when testing for the path from n4 to n2. In my situation I don't consider a cycle other paths because they are "safe" to render in the UI. However, if I would try to render the association between in n4 to n2 I would recreate the whole path recursively.

So far I came up with the following:

/**
 * @param {string} target The target node
 * @param {Record<string, string[]>} g The graph
 * @param {string} key The current key
 * @returns {boolean} true when going from `key` to `target` would create a cycle.
 */
hasCycle(target, g, key) {
  if (target === key) {
    return true;
  }
  
  const neighbors: string[] = [];
  Object.keys(g).forEach(prop => {
    if (g[prop].includes(key)) {
      neighbors.push(prop);
    }
  });
  
  for (const neighbor of neighbors) {
    if (neighbor === key) {
      return true;
    }
    if (hasCycle(target, g, neighbor)) {
      return true;
    }
  }
  return false;
}

This works in most cases except for the following:

n1: [n2]
n2: [n3]
n3: [n2]

When I test hasCycle('n2', {...}, 'n3') this function returns true as the cycle exist but for the purpose of the UI I am building I am not consider it a cycle just yet (I would if I would be testing for n3 -> n2).

Edit, to better illustrate the problem. Imagine rendering in the UI a JSON schema as a data structure. The schema has properties that can be either scalars or other schemes. These schemes can reference each other in properties which at some point will cause a cycle (when you represent it as a graph). The reason I am not considering n2 -> n3 a cycle for the purpose of the visualization is because it is safe for me to render the schema that is defined as n3 under a n2 property but if I would expand a property of n3 schema that has a reference to n2 would cause a recursive rendering of the whole tree.

- schema 1
  - prop1 (scalar)
  - prop2 (ref to schema 2)
- schema 2
  - prop3 (ref to schema 3)
- schema 3
  - prop4 (ref to schema 2)

In the UI you need to expand complex properties (referenced schemes) to see their properties. When the user expands prop4 it would recreate the whole schema 2 -> prop3 -> schema 3 -> prop4 in a cycle which would end with throwing an error. The function I am trying to find is to tell me that prop4 cannot be expanded.

Pawel Uchida-Psztyc
  • 3,735
  • 2
  • 20
  • 41
  • I prepared the following fiddle to illustrate the problem: https://jsfiddle.net/jarrodek/50kd2b17/46/ – Pawel Uchida-Psztyc May 15 '22 at 20:21
  • Not sure what you mean by "*going from `key` to `target` would create a cycle*". Your graph is static, right? It either has a cycle or it doesn't, and a cycle may contain certain edges or nodes. Are you trying to find a cycle in the graph that contains `n2` and `n4`? – Bergi May 15 '22 at 20:30
  • @Bergi It is static in this example. The whole graph is generated depending on user input. The `key` represents the current node and the `target` represents the path I can render. I want to render in the UI information that this path is recursive so it cannot be rendered. – Pawel Uchida-Psztyc May 15 '22 at 20:35
  • What is a "*recursive path*"? A path that contains a cycle, i.e. contains the same node multiple times? And no, a single `target` does not represent a path, there might be multiple (or no) paths between the source and the target node. – Bergi May 15 '22 at 20:39
  • @Bergi "recursive path" as a cycle. My mistake, I mean `target` is the target node and the path is between the two nodes. I believe the jsfiddle I linked in the first comment well illustrate the problem. – Pawel Uchida-Psztyc May 15 '22 at 20:44
  • A path between two different nodes is not a cycle. I don't understand the fiddle either. It has some "expected" values, but no explanation *why* these would be expected. Also I don't get why the path `n2 -> n3` would have a different expected result than the path `n3 -> n2`. – Bergi May 15 '22 at 20:55
  • @Bergi The "expected" means that the result of calling the function should equal to the value I set as the 3rd value in the array. The why is no important to helping me solve that but if you this helps you then I am rendering a complex data structure (think JSON schema or similar) where you can expand a property of the schema which can be another schema (if not a scalar property). These properties can be recursive (in graph terminology is a cycle) so rendering such a property would cause recursion and eventually an error. – Pawel Uchida-Psztyc May 15 '22 at 21:01
  • So actually you are doing a graph traversal, and you wan to find whether traversing an edge would *close* a cycle to subgraph of nodes that you already visited? – Bergi May 15 '22 at 21:09
  • @Bergi yes, actually. – Pawel Uchida-Psztyc May 15 '22 at 21:13
  • Thanks. The problem with your implementation is that this doesn't actually depend only on the current key, but rather on the path from starting point to the current key. – Bergi May 15 '22 at 21:19
  • @Bergi Just to clarify, in my case in a simple graph with two nodes, each referencing another (A -> B, B -> A) I need to consider `A -> B` as not a cycle but `B -> A` as a cycle. – Pawel Uchida-Psztyc May 15 '22 at 21:20
  • "*When the user expands `prop4` it would recreate the whole `schema 2` -> `prop3` -> `schema 3` -> `prop4` in a cycle*" - I don't follow. Wouldn't you just render the `schema 2` there - collapsed? And then the user would have to expand `prop3` again? This could recurse arbitrarily, without leading to an error. – Bergi May 15 '22 at 21:21
  • That you want to consider `B -> A` as closing the cycle but not `A -> B` depends on you starting in `A`. If you had started at node `B`, you'd get the opposite result. Also for your larger example, consider `schema 1` having a `prop5` that references `schema 3`. Now its either the `schema 2 -prop3-> schema 3` edge or the `schema3 -prop4 -> schema 2` that you don't want to expand, **depending** on whether you had previously expanded `schema 1 -prop2 -> schema 2` or `schema 1 -prop5-> schema 3`. – Bergi May 15 '22 at 21:25
  • @Bergi Generally you are right and I considered this. However, for the purpose of the application I should point out recursion in the data (that's one of the requirements). So I still need to know when exactly a cycle occurs. – Pawel Uchida-Psztyc May 15 '22 at 21:25
  • @Bergi Yes, now I realize the path is important and I need to consider the direction to solve this. Thanks. – Pawel Uchida-Psztyc May 15 '22 at 21:42
  • 1
    Good :-) At which point the solution just becomes `currentPath.includes(nextTarget)` – Bergi May 15 '22 at 21:44
  • 1
    @Bergi I used your advice and now it all works perfectly. Retrospectively, it was dumb not to think about the direction. I guess I am tired trying to make this work for 2 days... – Pawel Uchida-Psztyc May 15 '22 at 22:04

0 Answers0