Graph Traversal

Graph Traversal with DFS/BFS

Tyler Moore One of the most fundamental graph problems is to traverse every edge and vertex in a graph. CSE 3353, SMU, Dallas, TX For correctness, we must do the traversal in a systematic way so that we dont miss anything. Lecture 6 For efficiency, we must make sure we visit each edge at most twice. Since a maze is just a graph, such an must be powerful enough to enable us to get out of an arbitrary maze.

Some slides created by or adapted from Dr. Kevin Wayne. For more information see

http://www.cs.princeton.edu/~wayne/kleinberg-tardos

2 / 20

Marking Vertices To Do List

The key idea is that we must mark each vertex when wefirst visit it, We must also maintain a structure containing all the vertices we have and keep track of what have not yet completely explored. discovered but not yet completely explored. Each vertex will always be in one of the following three states: Initially, only a single start vertex is considered to be discovered. 1 undiscovered the vertex in its initial, virgin state. To completely explore a vertex, we look at each edge going out of it. 2 discovered the vertex after we have encountered it, but before we have checked out all its incident edges. For each edge which goes to an undiscovered vertex, we mark it 3 processed the vertex after we have visited all its incident edges. discovered and add it to the list of work to do. A vertex cannot be processed before we discover it, so over the course Note that regardless of what order we fetch the next vertex to of the traversal the state of each vertex progresses from undiscovered explore, each edge is considered exactly twice, when each of its to discovered to processed. endpoints are explored.

3 / 20 4 / 20 In what order should we process vertices? Depth-First Traversal

Undirected Graph Depth-First Search a 7 1

1 First-in-first-out: if we use a queue to process discovered vertices, we b b f use breadth-first search c 2 2 Last-in-first-out: if we use a stack to process discovered vertices, we use depth-first search a c 6 3

d 5 f e d 4 Discovered: a b c d e f Processed: e d c b f a e

5 / 20 6 / 20

Recursive Depth-First Traversal Code Iterative Depth-First Traversal Code

def iter d f s (G, s ) : S, Q= set(), [] #Visited set and queue def rec dfs(G, s, S=None): − i fS is None: S = set()# Initialize the history Q.append(s) #Weplanon v i s i t i n g s S . add ( s ) #We’ve v i s i t e d s whileQ: #Plan ned nodes left? f o ru in G[s]: #Explore neighbors u = Q. pop ( ) #Getone i fu inS: continue# Already visited: Skip i fu inS: continue# Already visited? Skip it r e c d f s (G, u , S) #New: Explore recursively S . add ( u ) #We’vevisited i t now Q.extend(G[u]) # Schedule all neighbors >>> rec d f s tested(G, 0) y i e l d u #Reportuas v i s i t e d [0, 1, 2, 3, 4, 5, 6, 7] >>> list(iter d f s (G, 0 ) ) [0, 5, 7, 6, 2, 3, 4, 1]

7 / 20 8 / 20 What does the yield command do? Breadth-First Traversal

Undirected Graph Breadth-First Search Tree return When you execute a function, it normally s values b a 3 Generators only execute code when iterated 1 c 2 Useful when you don’t need to keep the list of values you loop over 5 a e Especially helpful when the object you’re iterating over is very large, b f 4

and you don’t want to keep the entire object in memory d 6 7 Nice explanation on Stack Overflow: http://stackoverflow.com/ f e c d questions/231767/the-python-yield-keyword-explained Discovered: a b e f c d Processed: a b e f c d

9 / 20 10 / 20

Breadth-First Traversal Code Correctness of Graph Traversal

from collections import deque def iter bfs(G, s, S = None): Every edge and vertex in the connected component is eventually S, Q= set(), deque() # Visited set and queue visited. Why? − Q.append(s) #Weplanon v i s i t i n g s Suppose it’s not correct, ie. there exists an unvisited vertexA whose whileQ: #Plan ned nodes left? neighborB was visited. u = Q.popleft() #Getone WhenB was visited, each of its neighbors was added to the list to be i fu inS: continue# Already visited? Skip it processed. SinceA is a neighbor ofB, it must be visited before the S . add ( u ) #We’vevisited i t now algorithm completes. Q.extend(G[u]) # Schedule all neighbors y i e l d u #Reportuas v i s i t e d

11 / 20 12 / 20 Preorder vs. postorder processing Preorder and postorder processing with BFS

def bfs pp(G, s, S = None): S, Q= set(), deque() # Visited set and queue Q.append(s) #Weplano− n v i s i t i n g s w h i l eQ:# Planned nodes left? u = Q.popleft() #Getone i fu inS: continue# Already visited? Skip it p r i n t ’pre ’+u # preorder processing aka process vertex early (ADM) Many useful are built on top of BFS or DFS traversal S . add ( u ) #We’vevisite d i t now −− Q.extend(G[u]) # Schedule all neighbors Frequently, extra code is added either immediately before a node is y i e l d u #Reportu as v i s i t e d p r i n t’ p o s t ’+u # postorder processing aka process vertex late processed (preorder processing) or after a node is processed −−−−−− −− (postorder processing) >>> list(bfs p p (N, ’ a ’ ) ) p r e a post a p−−−−−− r e b post b p−−−−−− r e e post e p−−−−−− r e f post f p−−−−−− r e c post c p−−−−−− r e d post d [’a’,−−−−−− ’b’, ’e’, ’f’, ’c’, ’d ’]

13 / 20 14 / 20

Preorder and postorder processing with DFS DFS Trees: all descendants of a nodeu are processed after u is discovered but beforeu is processed def rec d f s pp(G, s, S=None): i fS is None: S = set()# Initialize the history Undirected Graph Depth-First Search Tree S . add ( s ) # We ’ve visited s p r i n t ’pre: ’+s # print node preorder processing f o ru in G[s]: #Explore −− neighbors a 10 i fu inS: continue# Already visited: Skip 1 r e c d f s p p (G, u , S ) # New: Explore recursively p r i n t’ p o s t : ’+s #print node postorder processing r e t u r n−−−−−−S −− b b f 2 >>> list(rec d f s p p (N, ’ a ’ ) ) c p r e : a p r e : b a c p r e : c 6 p r e : d 3

p r e : e d 5 post: e −−−−−− post: d −−−−−− post: c f e d −−−−−− post: b −−−−−−

p r e : f 4 post: f −−−−−− post: a Discovered: a b c d e f −−−−−−[’a’, ’c’, ’b’, ’e’, ’d’, ’f ’] Processed: e d c b f a e

15 / 20 16 / 20 How can we tell if one node is a descendant of another? Code for depth-first timestamps

def dfs(G, s, d, f, S=None, t=0): i fS is None: S = set()# Initialize the history d[s] = t; t+=1 #Set discover time Answer: with depth-first timestamps! S . add ( s ) #We’ve v i s i t e d s After we create a graph in a depth-first traversal, it would be nice to f o ru in G[s]: #Explore neighbors be able to verify if nodeA is encountered before nodeB, etc. i fu inS: continue# Already visited. Skip t = dfs(G, u, d, f, S, t) #Recurse ; update We add one timestamp for when a node is discovered (during preorder f[s] = t; t+=1 #Set finish time processing) and another timestamp for when a node is processed returnt #Retu rn timestamp (during postorder processing)

>>> f= >>> d={} >>> dfs(N,’a’,d,f){} 12 >>>d ’a’: 0, ’c’: 2, ’b’: 1, ’e’: 4, ’d’: 3, ’f’: 9 >>>{ f } ’a’: 11, ’c’: 7, ’b’: 8, ’e’: 5, ’d’: 6, ’f’: 10 { }

17 / 20 18 / 20

Edge cases for DFS/BFS traversal Edge classification

Tree edges Forward edge a a

b c b c 1 Edge (u,v) is a tree edge ifv wasfirst discovered by exploring edge (u,v). d e d e 2 Edge (u,v) is a back edge if vertexu is connected to an ancestorv. 3 Edge (u,v) is a forward edge if vertexu is connected to a Back edge Cross edges descendantv. a a 4 All other edges are cross edges

b c b c

d e d e

19 / 20 20 / 20