# Graph Traversals ## and Topological Sort --- CS 137 // 2021-10-25 ## Administrivia - Assignment 2 is due on Wednesday + Let me know if you need an extension - DE6 returned on Gradescope - If you haven't already, please fill out the [midterm evaluation questionnaire](https://docs.google.com/forms/d/e/1FAIpQLSeWe6Nd0ndk-XsfXjH9a3_B9lsRtY6yQFUtPltsXj8n2OoirA/viewform?usp=sf_link) + Need to be signed into Google with your Drake email address # Questions ## ...about anything? # Maze Solving ## Maze Solving ![Maze](https://d18l82el6cdm1i.cloudfront.net/image_optimizer/54429cab5dc05daafae44df6aae405409f240684.gif) ## Mazes are Just Graphs - Note that a maze can be modeled as a graph - Every "room" is a vertex and every "door" is an edge - Thus, maze solving is equivalent to searching through the paths of a graph ## Graph Adjacency List
%3
a
a
b
b
a->b
c
c
a->c
b->c
e
e
c->e
d
d
d->c
e->a
```py # Represents a graph as a dictionary (hashtable) that maps # vertices to a list of neighbors (i.e. adjacency list) test_graph = { "a" : ["b","c"], "b" : ["c"], "d" : ["c"], "c" : ["e"], "e" : ["a"] } test_graph["a"] # gets the neighbors of "a" ``` # Depth-First Search (DFS) ## Depth-First Search - Core idea: + Maintain a **set** of vertices we've seen already + Maintain a **stack** of vertices we've discovered but haven't processed yet + Repeatedly pop a vertex off the stack, push each unvisited neighbor to the stack and seen vertices ## DFS Visualization ![DFS Visualization](https://aquarchitect.github.io/swift-algorithm-club/Depth-First%20Search/Images/AnimatedExample.gif) ## DFS Pseudocode ```text dfs(g, start) Initialize set D = {start} Initialize stack S = (start) While S is not empty Let u = S.pop() Process the vertex u For each vertex, v, neighboring u If v is not in D Add v to D Push v to S ``` ## DFS Implementation ```py def dfs(g, start): discovered = {start} stack = [start] while len(stack) > 0: u = stack.pop() print(u) for v in g[u]: if v not in discovered: discovered.add(v) stack.append(v) ``` - Given a graph with $n$ vertices and $m$ edges, what is the runtime complexity of DFS? - $O(n + m)$, (each vertex/edge is touched once) - This is the "linear time" for graphs ## Using DFS to Solve Problems - Many graph problems reduce to a traversal like DFS - How can we use DFS to solve mazes? ## Keeping Track of Paths - It is possible to modify DFS in order to remember the path taken from the starting vertex - Need to remember the **parent** of discovered vertices ```py[2,12] def dfs(g, start): parents = {start : None} discovered = {start} stack = [start] while len(stack) > 0: u = stack.pop() print(u) for v in g[u]: if v not in discovered: discovered.add(v) stack.append(v) parents[v] = u ``` ## Reconstructing Path - Given `parents`, how can we reconstruct the path from `start` to a target vertex `end`? ```py def construct_path(parents, start, end): if end not in parents: # end isn't reachable return None elif start == end: return [start] else: prev = parents[end] path = construct_path(parents, start, prev) path.append(end) return path ``` ## Solving a Maze - We now have what we need to solve general mazes: ```py def solve_maze(g, start, end): parents = {start : None} discovered = {start} stack = [start] while len(stack) > 0: u = stack.pop() for v in g[u]: if v not in discovered: discovered.add(v) stack.append(v) parents[v] = u return construct_path(parents, start, end) ``` - Does this algorithm always return the shortest path? + No. Will return first path found even if long ## Finding the Shortest Path - For shortest path, we need another approach - Note that with DFS we always process the most recently discovered vertex + This causes us to go "deep" down a path and only turn around when we find a dead end - What if we process the vertices in a different order? ![BFS Maze Solving](https://thumbs.gfycat.com/OptimalWelldocumentedAnaconda-max-1mb.gif) # Breadth-First Search (BFS) ## Breadth-First Search (BFS) - The core idea behind BFS is the same as DFS except we process the vertices in a different order - Instead of processing the last discovered vertex, we will process vertices in the order we find them + Instead of a "last in first out" we want a "first in first out" ordering ## BFS Implementation ```py[1-12|5,12] from collections import deque def bfs(g, start): discovered = {start} queue = deque(discovered) while len(queue) > 0: u = queue.pop() print(u) for v in g[u]: if v not in discovered: discovered.add(v) queue.appendleft(v) ``` ## DFS and BFS Visualization ![BFS DFS Comparison](https://miro.medium.com/max/1280/1*GT9oSo0agIeIj6nTg3jFEA.gif) ## Exercise - BFS can be used to find the shortest path **from** a vertex $u$ to every other reachable vertex in the graph - Suppose we want to do something similar: + Find the shortest path **to** a vertex $u$ from every other vertex in the graph - With your table, come up with an algorithm for this + No need to write code---just the idea ## Exercise - A directed graph is **strongly connected** if for every pair of vertices, $u,v$, there is a path starting at $u$ and ending at $v$ - With your table, come up with an algorithm for this + No need to write code---just the idea # Topological Sort ## Prerequisite Graphs - An issue you might be familiar with is writing a schedule to take all the CS requirements in some particular order (four year plan) ---
%3
cs147
CS 147
Graphics
cs167
CS 167
Mach. Learn.
cs178
CS 178
Cloud
cs65
CS 65
CS I
cs65->cs167
cs66
CS 66
CS II
cs65->cs66
cs139
CS 139
Theory
cs65->cs139
cs66->cs147
cs66->cs178
cs146
CS 67
CS III
cs66->cs146
cs130
CS 130
Org I
cs66->cs130
cs135
CS 135
PL
cs146->cs135
cs137
CS 137
Algorithms
cs146->cs137
cs188
CS 188
SE
cs146->cs188
math54
MATH 54
Discrete Math
math54->cs167
math54->cs137
math54->cs139
cs83
CS 83
Ethics
## Directed Acyclic Graphs - A dependency graph like this is called a **directed acyclic graph** or **DAG** for short - Why doesn't it make sense for a dependency graph to have a cycle? + Cyclical dependencies means some courses cannot be taken without skipping a prerequisite ## Exercise - Write down a sensible ordering for a student to take the following courses ---
%3
cs147
CS 147
Graphics
cs167
CS 167
Mach. Learn.
cs178
CS 178
Cloud
cs65
CS 65
CS I
cs65->cs167
cs66
CS 66
CS II
cs65->cs66
cs139
CS 139
Theory
cs65->cs139
cs66->cs147
cs66->cs178
cs146
CS 67
CS III
cs66->cs146
cs130
CS 130
Org I
cs66->cs130
cs135
CS 135
PL
cs146->cs135
cs137
CS 137
Algorithms
cs146->cs137
cs188
CS 188
SE
cs146->cs188
math54
MATH 54
Discrete Math
math54->cs167
math54->cs137
math54->cs139
cs83
CS 83
Ethics
## Topological Ordering ![Topological Ordering](https://miro.medium.com/max/1400/1*uMg_ojFXts2WZSjcZe4oRQ.png) ## Topological Sort - An algorithm for finding a topological ordering is called a **topological sort** - Your process for composing a four-year plan is literally a topological sort - The core idea behind the topological sort algorithm is exactly the process you used to order the courses ## Topological Sort ![Topological Sort](https://raw.githubusercontent.com/yousefwalid/CMP302-Summary/master/assets/graphs/topological.gif) --- 1. Pick a vertex with in-degree zero (no prerequisites) and add it to our output ordering 2. Remove it from the graph 3. Repeat until no vertices are left ## Calculating the In-Degrees of Vertices ```py def in_degree(g): """Calculates the in-degree of all vertices""" degree = {u:0 for u in g} for u in g: for v in g[u]: degree[v] += 1 return degree ``` ## Topological Sort Implementation ```py def toposort(g): degree = in_degree(g) ready = [u for u in degree if degree[u] == 0] output = [] while len(ready) > 0: u = ready.pop() output.append(u) for v in g[u]: degree[v] -= 1 if degree[v] == 0: ready.append(v) return output ```