2

I am using cytoscape.js to create a graph.

I want to travel through the nodes (with direction), but only to those nodes who are connected with a edge that contains a specific value.

I have done it with recursion like this

function traverse(node, requiredValue, visitedNodes) {
  // break if we visit a node twice
  if (visitedNodes.map(visitedNode => visitedNode.id()).includes(node.id())) {
    return visitedNodes;
  }

  // add visited node to collection
  visitedNodes.push(node);

  // select node
  node.select();

  // only travel through valid edges
  const edgesTo = node.outgoers(function () {
    return this.isEdge() && this.data('requiredValues').includes(requiredValue);
  });

  // break if no valid connected edges
  if (edgesTo.empty()) {
    return visitedNodes;
  }

  // travel through edges
  edgesTo.forEach(edge => {
    edge.select()
    return traverse(edge.target(), agent, visitedNodes);
  });
}

It seems to work, but I am not very good at algorithms, so I am not sure if this is a clever way to build it. I have read a bit about breadth first and depth first search algorithms, but I am not sure if it's those algorithms that I need.

Would it be possible to traverse a tree without using recursion? I have also tried with a while loop, but since it is a tree, I don't think I can just use

node = rootNode;

while (true) {
  // find leaving edges
  ...

  edgesTo.forEach(edge => {
    // set new node to traverse
    node = edge.target;
  }
}

since I guess the edgesTo.forEach() will finish before moving on to the next iteration in the while loop. I need it to traverse 'simultaneously' through the different branches.

I can see from http://js.cytoscape.org/#collection/algorithms that the library has multiple algorithms (including bfs and dfs), but do I need those algorithms when I want to traverse the tree without the purpose of searching for one specific node?

Saeid
  • 4,147
  • 7
  • 27
  • 43
Jamgreen
  • 10,329
  • 29
  • 113
  • 224

3 Answers3

3

BFS and DFS are general graph traversal algorithms. They are not only for searching for some specific node. Many algorithms have BFS and DFS as a subroutine.

Your code basically performs a DFS on the graph. You are ignoring all unwanted edges and traversing the rest of the graph in a depth-first manner.

Yes it is possible to traverse the graph without recursion using both DFS and BFS and only use some specific edges. You just have to ignore the unwanted edges.

BFS:

Breadth-First-Search(Graph, root):
     create empty queue Q       
     visited.put(root)
     Q.enqueue(root)                      
     while Q is not empty:             
        current = Q.dequeue()     
        for each edge that edge.startpoint == current:
             if current has required value AND edge.endpoint is not in visited:
                 Q.enqueue(edge.endpoint)
                 visited.put(edge.endpoint)

DFS:

procedure DFS-iterative(G,v):
      let S be a stack
      S.push(v)
      while S is not empty:
          v = S.pop()
          if v is not labeled as discovered:
              label v as discovered
              for all edges from v to w in G.adjacentEdges(v) :
                  if edge has required value:
                       S.push(w)
Saeid
  • 4,147
  • 7
  • 27
  • 43
  • Thanks! I will try to implement it with your code. But in your DFS code snippet, you have `if ... and w is not discovered` in second last line and `if v is not labeled as discovered` in your fifth last line. Isn't it redundant to check this twice? – Jamgreen Oct 01 '16 at 14:05
2

I'll chop my answer down to several questions you asked either explicitly or implicitly:

BFS and DFS algorithms in general

BFS and DFS are algorithms to traverse a connected graph (or a connection component of a graph). They allow you to visit all the connected nodes from a specific starting node (they do not search for a specific node as you hinted, although they can be used in such a manner), and differ in the order in which they traverse the graph (Breadth-Frist, meaning all the immediate neighbours of a node are visited before going into the next layer of neighbours, compared to Depth-First, meaning pursuing a branch that starts with one immediate neighbour first and going deeper, visiting all the nodes that are connected to the starting node through that particular neighbour node before continuing to the next branch, which are all the nodes connected through the next immediate neighbour).

BFS and DFS relating to your requirements

As before, both algorithms visit ALL the nodes in a connected component of a graph (all the nodes that can be reached from the starting node by traversing edges). As you have a different requirement (traverse using only edges of specific values) I'd suggest you implement your own alteration of either of the algorithms, adding this demand. You have actually already done that: your algorithm is a descendant/version of DFS in a sense.

BFS and DFS and recursion

BFS does not normally include recursion and uses a data structure (a queue) to achieve its traversal order.

DFS is easily implemented with recursion (and your intuitive way of implementing your algorithm is very similar to that). But you can implement DFS without recursion - using a data structure (a stack) to achieve its traversal order (essentially the recursion uses the call stack as the data structure implicitly so it isn't that different, although the recursion have more overhead to deal with the function calls and their environments). You can see a pseudocode in the wiki of DFS. Here it is for convenience:

procedure DFS-iterative(G,v):
  let S be a stack
  S.push(v)
  while S is not empty
      v = S.pop()
      if v is not labeled as discovered:
          label v as discovered
          for all edges from v to w in G.adjacentEdges(v) do
              S.push(w)
Community
  • 1
  • 1
et_l
  • 1,868
  • 17
  • 29
  • Thanks for a great answer. The problem is just that if I look at http://js.cytoscape.org/#eles.depthFirstSearch, I only get the argument `e` which represents the edge connecting the previous node to the current node, so using this pre-implemented algorithm, I cannot control whether it should have visited this node, because it is already done. Inside the `visit` parameter function in the `options` argument, I could do something like `if (e && !e.data('reqValues').includes(reqValue)) {..`, but then it will end the `visit` function before visiting the remaining nodes, so it is also not an option. – Jamgreen Oct 01 '16 at 13:44
  • 1
    You are right, I don't think you should use this ready-made function but rather implement it yourself. That's what I wrote in "BFS and DFS relating to your requirements". In "BFS and DFS and recursion" I provided you with the DFS loop-instead-of-recursion pseudocode so you can add your requirement to the `for` (basically changing it to `for all edges from v to w in G.adjacentEdges(v) that are of the value requiredValue do`). But the answer Tempux added includes the implementations of both algorithms including the adjustments for your requirements. – et_l Oct 02 '16 at 07:09
  • `visit()` is not for specifying which elements may be traversed. It's for specifying a goal, or for iteration/traversal callbacks. Just [filter](http://js.cytoscape.org/#eles.filter) the elements to specify the subset of the graph you want to run BFS/DFS on. A lot of literature on graph theory implicitly uses the whole graph for algorithms, so it's easy to forget that you can use subgraphs. – maxkfranz Oct 03 '16 at 15:35
0

Cytoscape includes many common graph theory algorithms out of the box. The algorithms even let you to specify which elements to include / call on.

You could re-implement the BFS traversal algorithm yourself using low-level traversal APIs like .outgoers(), but why re-invent the wheel?

BFS and DFS are built into Cytoscape:

cy.elements().stdFilter(function( ele ){
  return ele.isNode() ? includeThisNode( ele ) : includeThisEdge( ele ); // eles to include
}).bfs({
  visit: function(){ ... } // called when new ele visited
});

Specify includeThisNode() and includeThisEdge() to meet your criteria for which elements should be allowed to be traversed.

If you have a goal node with certain criteria, return true in visit() when those conditions are met.

maxkfranz
  • 11,896
  • 1
  • 27
  • 36
  • Great library! :-D I don't know how it's implemented, but it looks like it will first get all valid elements, but some nodes will be treated as valid even though the traversal won't visit the node because no valid edges lead to it. So isn't this ineffective with `cy.elements.filter()` returning an unnecessarily big collection compared to an algorithm that only considers nodes to which it legally can travel? I could maybe use `ele.isNode() ? ele.connectedEdges().filterFn(edge => edge.target() === ele && edge.data('agents').includes(agent) : ele.data('agents').includes(agent)`. – Jamgreen Oct 04 '16 at 04:40
  • The only restriction that's important is exclusion. You must exclude nodes and edges that you absolutely don't want traversed. Just let BFS handle reachability for you. – maxkfranz Oct 05 '16 at 15:23