diff --git a/Labs/Lab5/dfs-class/README.md b/Labs/Lab5/dfs-class/README.md new file mode 100644 index 0000000000000000000000000000000000000000..35b27469feaf64ccd1a6c92162b99111ccd946bc --- /dev/null +++ b/Labs/Lab5/dfs-class/README.md @@ -0,0 +1,157 @@ +# Inheritance and DFS + +## Inheritance + +Paste and run the following code in a new python notebook called debug.ipynb in your p3 folder: + +```python +class Parent: + def twice(self): + self.message() + self.message() + + def message(self): + print("parent says hi") + +class Child: + def message(self): + print("child says hi") + +c = Child() +``` + +Modify `Child` so that it inherits from `Parent`. + +What do you think will be printed if you call `c.twice()`? Discuss +with your group, then run it to find out. + +When `self.some_method(...)` is called, and there are multiple methods +named `some_method` in your program, the type of `self` (the original object that is calling the method from the class) is what +matters for determining which one runs. It doesn't matter where the +`self.some_method(...)` is (could be any method). + +## GraphSearcher + +Copy and paste the following starter code (which you'll build on in the project): + +```python +class GraphSearcher: + def __init__(self): + self.visited = set() + self.order = [] + + def visit_and_get_children(self, node): + """ Record the node value in self.order, and return its children + param: node + return: children of the given node + """ + raise Exception("must be overridden in sub classes -- don't change me here!") + + def dfs_search(self, node): + # 1. clear out visited set and order list + # 2. start recursive search by calling dfs_visit + + def dfs_visit(self, node): + # 1. if this node has already been visited, just `return` (no value necessary) + # 2. mark node as visited by adding it to the set + # 3. call self.visit_and_get_children(node) to get the children + # 4. in a loop, call dfs_visit on each of the children +``` + +The graphs we search on come in many shapes and formats +(e.g. matrices, files or web), but it would be nice if we could use +the same depth-first search (DFS) code when we want to search +different kinds of graphs. Therefore, we would like to implement a +base class `GraphSearcher` and implement the DFS algorithm in it. + +For our purposes, we aren't using DFS to find a specific path. We +just want to see what nodes are reachable from a given starting +`node`, so these methods don't need to return any value. Your job is +to replace the comments in `dfs_search` and `dfs_visit` with code +(some comments may require a couple lines of code). + +The `dfs_visit` method will call `visit_and_get_children` to record +the node value and determine the children of a given node. Subclasses +of `GraphSearcher` can override `visit_and_get_children` to lookup the +children of a node in different kinds of graphs (e.g. matrices, files +or web). + + +Try your code: + +```python +g = GraphSearcher() +g.dfs_search("A") +``` + +You should get an exception. The purpose of `GraphSearcher` is not to +directly create objects, it is to let other clases inherit +`dfs_search` (we'll do the inheritance soon). + +## Matrix Format + +Paste and run the following: + +```python +import pandas as pd + +df = pd.DataFrame([ + [0,1,0,1], + [0,0,1,0], + [0,0,0,1], + [0,0,1,0], +], index=["A", "B", "C", "D"], columns=["A", "B", "C", "D"]) +df +``` + +A grid of ones and zeros like this is a common way to represent +directed graphs. A `1` in the "C" column of the "B" row means that +there is an edge from node B to node C. + +Try drawing a directed graph on a piece of paper based on the above +grid. + +`df.loc["????"]` looks up a row in a DataFrame. Use it to lookup the +children of node B. + +Complete the following to print all the children of "B" (should only be "C"): + +```python +for node, has_edge in df.loc["B"].items(): + if ????: + print(????) +``` + +Let's create a class that inherits from `GraphSearcher` and works with +graphs represented as matrices: + +```python +class MatrixSearcher(????): + def __init__(self, df): + super().????() # call constructor method of parent class + self.df = df + + def visit_and_get_children(self, node): + # TODO: Record the node value in self.order + children = [] + # TODO: use `self.df` to determine what children the node has and append them + return children +``` + +Complete the `????` and `TODO` parts. Test it, checking what nodes +are reachable from each starting point: + +```python +m = MatrixSearcher(df) +m.dfs_search(????) +m.order +``` + +From "A", for example, `m.order` should be `['A','B', 'C', 'D']`. Look +back at the picture you drew of the graph and make sure you're getting +what you expect when starting from other nodes. + +## scrape.py + +If you've been doing this work in a notebook, you should now move your +code to a new module called `scrape.py` in your `p3` directory. diff --git a/Labs/Lab5/dfs-vs-bfs/1.png b/Labs/Lab5/dfs-vs-bfs/1.png new file mode 100644 index 0000000000000000000000000000000000000000..1f79e63acf89a3ae0a3ed4afc8d10485aeaffcf5 Binary files /dev/null and b/Labs/Lab5/dfs-vs-bfs/1.png differ diff --git a/Labs/Lab5/dfs-vs-bfs/2.png b/Labs/Lab5/dfs-vs-bfs/2.png new file mode 100644 index 0000000000000000000000000000000000000000..872e2d34696bf058238b35b688fa5dd73c3f0a62 Binary files /dev/null and b/Labs/Lab5/dfs-vs-bfs/2.png differ diff --git a/Labs/Lab5/dfs-vs-bfs/3.png b/Labs/Lab5/dfs-vs-bfs/3.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6adce9b308f4a8366c439a7678963dead0462f Binary files /dev/null and b/Labs/Lab5/dfs-vs-bfs/3.png differ diff --git a/Labs/Lab5/dfs-vs-bfs/4.png b/Labs/Lab5/dfs-vs-bfs/4.png new file mode 100644 index 0000000000000000000000000000000000000000..ccf2b07d6d7804cbd47fcb3a236c0597ce26c595 Binary files /dev/null and b/Labs/Lab5/dfs-vs-bfs/4.png differ diff --git a/Labs/Lab5/dfs-vs-bfs/README.md b/Labs/Lab5/dfs-vs-bfs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4a470749c0eca2a5862cf3c8100dd53744b0f655 --- /dev/null +++ b/Labs/Lab5/dfs-vs-bfs/README.md @@ -0,0 +1,231 @@ +# DFS vs. BFS + +In this lab, you'll get practice with depth-first search and +breadth-first search with some interactive exercises. + +Start a new notebook on your virtual machine, then paste+run this code +in a cell (you don't need to read it): + +```python +from IPython.display import display, HTML +from graphviz import Digraph + +class test_graph: + def __init__(self): + self.nodes = {} + self.traverse_order = None # in what order were nodes checked? + self.next_guess = 0 + self.colors = {} + + def node(self, name): + name = str(name).upper() + self.nodes[name] = Node(self, name) + + def edge(self, src, dst): + src, dst = str(src).upper(), str(dst).upper() + for name in [src, dst]: + if not name in self.nodes: + self.node(name) + self.nodes[src].children.append(self.nodes[dst]) + + def _repr_svg_(self): + g = Digraph(engine='neato') + for n in self.nodes: + g.node(n, fillcolor=self.colors.get(n, "white"), style="filled") + children = self.nodes[n].children + for i, child in enumerate(children): + g.edge(n, child.name, penwidth=str(len(children) - i), len="1.5") + return g._repr_image_svg_xml() + + def dfs(self, src, dst): + src, dst = str(src).upper(), str(dst).upper() + self.traverse_order = [] + self.next_guess = 0 + self.colors = {} + self.visited = set() + self.path = self.nodes[src].dfs(dst) + display(HTML("now call .visit(???) to identify the first node explored")) + display(self) + + def bfs(self, src, dst): + src, dst = str(src).upper(), str(dst).upper() + self.traverse_order = [] + self.next_guess = 0 + self.colors = {} + self.path = self.nodes[src].bfs(dst) + display(HTML("now call .visit(???) to identify the first node explored")) + display(self) + + def visit(self, name): + name = str(name).upper() + if self.traverse_order == None: + print("please call dfs or bfs first") + if self.next_guess == len(self.traverse_order): + print("no more nodes to explore") + return + self.colors = {} + for n in self.traverse_order[:self.next_guess]: + self.colors[n] = "yellow" + if name == self.traverse_order[self.next_guess]: + display(HTML("Correct...")) + self.colors[name] = "yellow" + self.next_guess += 1 + else: + display(HTML("<b>Oops!</b> Please guess again.")) + self.colors[name] = "red" + display(self) + if self.next_guess == len(self.traverse_order): + if self.path == None: + display(HTML("You're done, there is no path!")) + else: + seq = input("What path was found? [enter nodes, comma separated]: ") + seq = tuple(map(str.strip, seq.upper().split(","))) + if seq == tuple(map(str.upper, self.path)): + print("Awesome!!!") + else: + print("actually, expected was: ", ",".join(self.path)) + + +class Node: + def __init__(self, graph, name): + self.graph = graph + self.name = name + self.children = [] + + def __repr__(self): + return "node %s" % self.name + + def dfs(self, dst): + if self.name in self.graph.visited: + return None + + self.graph.traverse_order.append(self.name) + + self.graph.visited.add(self.name) + + if self.name == dst: + return (self.name, ) + for child in self.children: + childpath = child.dfs(dst) + if childpath: + return (self.name, ) + childpath + return None + + def backtrace(self): + nodes = [] + node = self + while node != None: + nodes.append(node.name) + node = node.back + return tuple(reversed(nodes)) + + def bfs(self, dst): + added = set() + todo = [self] + self.back = None + added.add(self.name) + + while len(todo) > 0: + curr = todo.pop(0) + self.graph.traverse_order.append(curr.name) + + if curr.name == dst: + return curr.backtrace() + else: + for child in curr.children: + if not child.name in added: + todo.append(child) + child.back = curr + added.add(child.name) + + return None +``` + +## Problem 1 [4-node, DFS] + +Paste the following to a cell: + +```python +g = test_graph() +g.edge(1, 2) +g.edge(4, 3) +g.edge(1, 3) +g.edge(2, 4) +g +``` + +It should look something like this: + +<img src="1.png" width=300> + +Node 1 has two children: nodes 2 and 3. The thicker line to node 2 +indicates node 2 is in the `children` list before node 3. + +Let's do a DFS from node 1 to 3. Paste the following: + +```python +g.dfs(1, 3) +``` + +You should see something like this: + +<img src="2.png" width=500> + +Try calling the visit function with + +```python +g.visit(1) +``` + +The visited node should look like this: + +<img src="3.png" width=300> + +Keep making `g.visit(????)` calls until you complete the depth first search. + +Once the target node is reach, you'll be prompted to enter the path +from source to destination. Do so and type enter to check your +answer: + +<img src="4.png" width=600> + +## Problem 2 [4-node, BFS] + +Paste+run the following(same graph structure as last time, but you'll +visit the nodes in a different order by doing a BFS): + +```python +g = test_graph() +g.edge(1, 2) +g.edge(4, 3) +g.edge(1, 3) +g.edge(2, 4) +g.bfs(1, 3) +``` + +## Problem 3 [7-node, DFS+BFS] + +Paste+run the following: + +```python +g = test_graph() +for i in range(5): + g.edge(i, i+1) + g.edge(i, 6) + g.edge(6, i) +g.dfs(0, 4) +``` + +Then change `dfs` to `bfs` and try again. + +## Problem 4 [6-node, BFS] + +```python +g = test_graph() +for i in range(0, 4, 2): + g.edge(i, i+2) + g.edge(i+1, i+3) + g.edge(i, i+1) +g.edge(4, 5) +g.bfs(2, 1) +``` diff --git a/Labs/Lab5/lab5.md b/Labs/Lab5/lab5.md new file mode 100644 index 0000000000000000000000000000000000000000..fd8b4a115df9cad2bb3fb0947940daa7c4ffd2de --- /dev/null +++ b/Labs/Lab5/lab5.md @@ -0,0 +1,5 @@ +# Lab 5: Graph Search + +1. Practice [graph search order](./dfs-vs-bfs) + +2. Start the [module](./dfs-class) you'll be building for P3 \ No newline at end of file