OVERVIEW
I'm trying to figure out how to traverse directed cyclic graphs using some sort of DFS iterative algorithm. Here's a little mcve version of what I currently got implemented (it doesn't deal with cycles):
class Node(object): def __init__(self, name): self.name = name def start(self): print '{}_start'.format(self) def middle(self): print '{}_middle'.format(self) def end(self): print '{}_end'.format(self) def __str__(self): return "{0}".format(self.name) class NodeRepeat(Node): def __init__(self, name, num_repeats=1): super(NodeRepeat, self).__init__(name) self.num_repeats = num_repeats def dfs(graph, start): """Traverse graph from start node using DFS with reversed childs""" visited = {} stack = [(start, "")] while stack: # To convert dfs -> bfs # a) rename stack to queue # b) pop becomes pop(0) node, parent = stack.pop() if parent is None: if visited[node] < 3: node.end() visited[node] = 3 elif node not in visited: if visited.get(parent) == 2: parent.middle() elif visited.get(parent) == 1: visited[parent] = 2 node.start() visited[node] = 1 stack.append((node, None)) # Maybe you want a different order, if it's so, don't use reversed childs = reversed(graph.get(node, [])) for child in childs: if child not in visited: stack.append((child, node)) if __name__ == "__main__": Sequence1 = Node('Sequence1') MtxPushPop1 = Node('MtxPushPop1') Rotate1 = Node('Rotate1') Repeat1 = NodeRepeat('Repeat1', num_repeats=2) Sequence2 = Node('Sequence2') MtxPushPop2 = Node('MtxPushPop2') Translate = Node('Translate') Rotate2 = Node('Rotate2') Rotate3 = Node('Rotate3') Scale = Node('Scale') Repeat2 = NodeRepeat('Repeat2', num_repeats=3) Mesh = Node('Mesh') cyclic_graph = { Sequence1: [MtxPushPop1, Rotate1], MtxPushPop1: [Sequence2], Rotate1: [Repeat1], Sequence2: [MtxPushPop2, Translate], Repeat1: [Sequence1], MtxPushPop2: [Rotate2], Translate: [Rotate3], Rotate2: [Scale], Rotate3: [Repeat2], Scale: [Mesh], Repeat2: [Sequence2] } dfs(cyclic_graph, Sequence1) print '-'*80 a = Node('a') b = Node('b') dfs({ a : [b], b : [a] }, a)
The above code is testing a couple of cases, the first would be some sort of representation of the below graph:
The second one is the simplest case of one graph containing one "infinite" loop {a->b, b->a}
REQUIREMENTS
Repeat
where you can indicate how many iterations to loop around the cycleQUESTION
Could anyone provide some sort of solution which knows how to traverse infinite/finite cycles?
REFERENCES
If question is not clear yet at this point, you can read this more about this problem on this article, the whole idea will be using the traversal algorithm to implement a similar tool like the shown in that article.
Here's a screenshot showing up the whole power of this type of data structure I want to figure out how to traverse&run:
Since the graph is cyclic (i.e. can contain cycles), I would first break it down into strongly connected components. A strongly connected component of a directed graph is a subgraph where each node is reachable from every other node in the same subgraph. This would yield a set of subgraphs.
Depth First Traversal can be used to detect a cycle in a Graph. DFS for a connected graph produces a tree. There is a cycle in a graph only if there is a back edge present in the graph. A back edge is an edge that is joining a node to itself (self-loop) or one of its ancestor in the tree produced by DFS.
To detect cycle, check for a cycle in individual trees by checking back edges. To detect a back edge, keep track of vertices currently in the recursion stack of function for DFS traversal. If a vertex is reached that is already in the recursion stack, then there is a cycle in the tree.
Depth First Search (DFS) is a systematic way of visiting the nodes of either a directed or an undirected graph. As with breadth first search, DFS has a lot of applications in many problems in Graph Theory. It comprises the main part of many graph algorithms.
Before I start, Run the code on CodeSkulptor! I also hope that the comments elaborate what I have done enough. If you need more explanation, look at my explanation of the recursive approach below the code.
# If you don't want global variables, remove the indentation procedures indent = -1 MAX_THRESHOLD = 10 INF = 1 << 63 def whitespace(): global indent return '| ' * (indent) class Node: def __init__(self, name, num_repeats=INF): self.name = name self.num_repeats = num_repeats def start(self): global indent if self.name.find('Sequence') != -1: print whitespace() indent += 1 print whitespace() + '%s_start' % self.name def middle(self): print whitespace() + '%s_middle' % self.name def end(self): global indent print whitespace() + '%s_end' % self.name if self.name.find('Sequence') != -1: indent -= 1 print whitespace() def dfs(graph, start): visits = {} frontier = [] # The stack that keeps track of nodes to visit # Whenever we "visit" a node, increase its visit count frontier.append((start, start.num_repeats)) visits[start] = visits.get(start, 0) + 1 while frontier: # parent_repeat_count usually contains vertex.repeat_count # But, it may contain a higher value if a repeat node is its ancestor vertex, parent_repeat_count = frontier.pop() # Special case which signifies the end if parent_repeat_count == -1: vertex.end() # We're done with this vertex, clear visits so that # if any other node calls us, we're still able to be called visits[vertex] = 0 continue # Special case which signifies the middle if parent_repeat_count == -2: vertex.middle() continue # Send the start message vertex.start() # Add the node's end state to the stack first # So that it is executed last frontier.append((vertex, -1)) # No more children, continue # Because of the above line, the end method will # still be executed if vertex not in graph: continue ## Uncomment the following line if you want to go left to right neighbor #### graph[vertex].reverse() for i, neighbor in enumerate(graph[vertex]): # The repeat count should propagate amongst neighbors # That is if the parent had a higher repeat count, use that instead repeat_count = max(1, parent_repeat_count) if neighbor.num_repeats != INF: repeat_count = neighbor.num_repeats # We've gone through at least one neighbor node # Append this vertex's middle state to the stack if i >= 1: frontier.append((vertex, -2)) # If we've not visited the neighbor more times than we have to, visit it if visits.get(neighbor, 0) < MAX_THRESHOLD and visits.get(neighbor, 0) < repeat_count: frontier.append((neighbor, repeat_count)) visits[neighbor] = visits.get(neighbor, 0) + 1 def dfs_rec(graph, node, parent_repeat_count=INF, visits={}): visits[node] = visits.get(node, 0) + 1 node.start() if node not in graph: node.end() return for i, neighbor in enumerate(graph[node][::-1]): repeat_count = max(1, parent_repeat_count) if neighbor.num_repeats != INF: repeat_count = neighbor.num_repeats if i >= 1: node.middle() if visits.get(neighbor, 0) < MAX_THRESHOLD and visits.get(neighbor, 0) < repeat_count: dfs_rec(graph, neighbor, repeat_count, visits) node.end() visits[node] = 0 Sequence1 = Node('Sequence1') MtxPushPop1 = Node('MtxPushPop1') Rotate1 = Node('Rotate1') Repeat1 = Node('Repeat1', 2) Sequence2 = Node('Sequence2') MtxPushPop2 = Node('MtxPushPop2') Translate = Node('Translate') Rotate2 = Node('Rotate2') Rotate3 = Node('Rotate3') Scale = Node('Scale') Repeat2 = Node('Repeat2', 3) Mesh = Node('Mesh') cyclic_graph = { Sequence1: [MtxPushPop1, Rotate1], MtxPushPop1: [Sequence2], Rotate1: [Repeat1], Sequence2: [MtxPushPop2, Translate], Repeat1: [Sequence1], MtxPushPop2: [Rotate2], Translate: [Rotate3], Rotate2: [Scale], Rotate3: [Repeat2], Scale: [Mesh], Repeat2: [Sequence2] } dfs(cyclic_graph, Sequence1) print '-'*40 dfs_rec(cyclic_graph, Sequence1) print '-'*40 dfs({Sequence1: [Translate], Translate: [Sequence1]}, Sequence1) print '-'*40 dfs_rec({Sequence1: [Translate], Translate: [Sequence1]}, Sequence1)
The input and (well formatted and indented) output can be found here. If you want to see how I formatted the output, please refer to the code, which can also be found on CodeSkulptor.
Right, on to the explanation. The easier to understand but much more inefficient recursive solution, which I'll use to help explain, follows:
def dfs_rec(graph, node, parent_repeat_count=INF, visits={}): visits[node] = visits.get(node, 0) + 1 node.start() if node not in graph: node.end() return for i, neighbor in enumerate(graph[node][::-1]): repeat_count = max(1, parent_repeat_count) if neighbor.num_repeats != INF: repeat_count = neighbor.num_repeats if i >= 1: node.middle() if visits.get(neighbor, 0) < MAX_THRESHOLD and visits.get(neighbor, 0) < repeat_count: dfs_rec(graph, neighbor, repeat_count, visits) node.end() visits[node] = 0
start
event of the node.end
event and return.graph[node][::-1]
) in the recursive version to maintain the same order (right to left) of traversal of neighbors as in the iterative version. middle
event of the current node (not the neighbor) if the second (or greater) neighbor is being processed.MAX_THRESHOLD
times (for pseudo-infinite cycles) and b) the above calculated repeat count times.end
event and clear its visits in the hashtable. This is done so that if some other node calls it again, it does not fail the visitability check and/or execute for less than the required number of times.As per comment66244567 - reducing the graph to a tree by ignoring links to visited nodes and performing a breadth-first search, as this would produce a more natural-looking (and likely more balanced) tree:
def traverse(graph,node,process): seen={node} current_level=[node] while current_level: next_level=[] for node in current_level: process(node) for child in (link for link in graph.get(node,[]) if link not in seen): next_level.append(child) seen.add(child) current_level=next_level
With your graph and def process(node): print node
, this produces:
In [24]: traverse(cyclic_graph,Sequence1,process) Sequence1 MtxPushPop1 Rotate1 Sequence2 Repeat1 MtxPushPop2 Translate Rotate2 Rotate3 Scale Repeat2 Mesh
The other BFS algorithm, iterative deepening DFS (uses less memory at the cost of speed) isn't going to win you anything in this case: since you have to store references to visited nodes, you already consume O(n) memory. Neither you need to produce intermediate results (but you can anyway - e.g. yield
something after processing a level).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With