CS51 Session 6 - (3.2) Algorithms II Searching Graphs PDF

Title CS51 Session 6 - (3.2) Algorithms II Searching Graphs
Course Programming Concepts
Institution Southern Methodist University
Pages 4
File Size 83.8 KB
File Type PDF
Total Downloads 84
Total Views 121

Summary

Lecture notes for Session 6...


Description

CS51 Session 5 - (3.1) Algorithms I: Sorting We explore algorithms in the context of searching graph structures. We introduce two common broad classes of algorithms: brute-force and greedy. This will also involve a brief introduction to different types of data structures in Python: stacks and queues. In cryptography, a brute-force attack consists of an attacker submitting many passwords or passphrases with the hope of eventually guessing a combination correctly. The attacker systematically checks all possible passwords and passphrases until the correct one is found. Generates all possible solution candidates. - check one by one. Brute force algorithms are simple to implement but computationally intensive to run. They are reasonable solutions when n is small but even for moderately large values of n the solutions become too intensive to produce results in a reasonable time frame. One advantage of the the brute force algorithms is that they give the optimal solution. A greedy algorithm is any algorithm that follows the problem-solving heuristic of making the locally optimal choice at each stage. At each step choose the optimal solution. - the problem is confusing local and global extremum points. Greedy Algorithms are simple, straightforward and short sighted. They are easy to implement and sometimes produce results that we can live with. In a greedy algorithm you are always looking for the immediate gain without considering the long term effect. Even though you get short term gains with a greedy algorithm, it does not always produce the optimal solution. Of course, in order to find paths with certain properties one must first be able to search through graphs in a structured way. And so we will start our investigation of graph search algorithms with the most basic kind of graph search algorithms: the depth-first and breadth-first search. For the purpose of finding paths and simplicity in our derivations, we will impose two further conditions. First, the graphs must be simple. That is, no graph may have more than one edge between two vertices or self-adjacent vertices (that is, (v,v) can not be an edge for any vertex v). Second, the graphs must be connected. That is, all pairs of vertices must have a path connecting them.

Depth-First Search Our first algorithm will solve this problem quite nicely, and is called the depth-first search. Depth-first search  is inherently a recursion: 1) Start at a vertex. 2) Pick any unvisited vertex adjacent to the current vertex, and check to see if this is the goal.

3) If not, recursively apply the depth-first search to that vertex, ignoring any vertices that have already been visited. 4) Repeat until all adjacent vertices have been visited. There are a few tricky things going on in this code snippet. First, after checking the current Node for the sought value, we add the current Node to the set of visitedNodes. While subsequently iterating over the adjacent Nodes we check to make sure the Node has not been visited before recursing. Since Python passes these sets by reference, changes made to visitedNodes deep in the recursion persist after the recursive call ends. That is, much the same as lists in Python, these updates mutate the object. Of course, this still doesn’t fix the problem with too many recursive calls; a graph which is too large will cause an error because Python limits the number of recursive calls allowed. The next and final step is a standard method of turning a recursive algorithm into a non-recursive one, and it requires the knowledge of a particular data structure called a stack. In the abstract, stack stores a collection of items and has the following operations: - You can quickly add something to the structure. - You can quickly remove the most recently added item. These two operations are called push (to add) and pop (to remove). As a completely irrelevant aside, this is not the only algorithmic or mathematical concept based on pancakes. Luckily for us, Python’s lists double as stacks. At each vertex, push the adjacent vertices onto a stack in the reverse order that you iterate through the list. To choose which vertex to process next, simple pop whatever is on top of the stack, and process it (taking the stack with you as you go). Once again, we have to worry about which vertices have already been visited. This is precisely the idea of recursion: we don’t finish the recursive call until all of the neighbors are processed, and that in turn requires the processing of all of the neighbors’ neighbors, and so on.

Breadth-First Search As the name suggests, the breadth-first search operates in the “opposite” way from the depth-first search. Intuitively the breadth-first search prefers to visit the neighbors of earlier visited nodes before the neighbors of more recently visited ones. Starting again with vertex 1, we add 4, 3, and 2 (in that order) to our data structure, but now we prefer the first thing added to our data structure instead of the last. That is, in the next step we visit vertex 4 instead of vertex 2. Since vertex 4 is adjacent to nobody, the recursion ends and we continue with vertex 3.

Notice the pattern  here: after processing vertex 1, we processed all of the neighbors of vertex 1 before processing any vertices not immediately adjacent to vertex one. This is where the “breadth” part distinguishes this algorithm from the “depth” part. Metaphorically, a breadth-first search algorithm will look all around a vertex before continuing on into the depths of a graph, while the depth-first search will dive straight to the bottom of the ocean before looking at where it is. The way that we’ll make these rules rigorous is in the data-structure version of the algorithm: instead of using a stack we’ll use a queue. Again in the abstract, a queue is a data structure for which we’d like the following properties: - We can quickly add items to the queue. - We can quickly remove the least recently added item. The operations on a queue are usually called enqueue (for additions) and dequeue (for removals). Again, Python’s lists have operations that functionally make them queues, but the analogue of the enqueue operation is not efficient (specifically, it costs O(n) for a list of size n). Generalizing After all of this exploration, it is clear that the depth-first search and the breadth-first search are truly the same algorithm. Indeed, the only difference is in the data structure, and this can be abstracted out of the entire procedure. Say that we have some data structure that has three operations: add, remove, and len (the Pythonic function for “query the size”). Then we can make a search algorithm that uses this structure without knowing how it works on the inside. Since words like stack, queue, and heap are already taken for specific data structures, we’ll call this arbitrary data structure a pile. Breadth-first searching (BFS) is an algorithm for traversing or searching a path in a graph. It starts at some arbitrary node of the graph and explores the neighboring nodes first, before moving to the next level neighbors. For BFS we are using a queue to store the nodes which will be exploring. This way we check the closest nodes first. It should look like we go level by level and before we move on to look at the next level, we should check all nodes in the current level. For a graph search, it's very important to write all of the visited nodes, or we can get ourselves into a loop. Time complexity for BFS (worst case) is O(|V|+|E|), where |V| is a number of nodes and |E| is a number of edges in the graph. Depth-first search (DFS) is an algorithm similar to BFS. It starts at some arbitrary node of the graph like BFS, but explores as far as possible along each branch. For a DFS non-recursive implementation, we are using a stack instead of a queue to store nodes which will be exploring. This way we check the nodes first which were last added to the stack.

DFS is not optimal and it is not guaranteed to find the best solution. This means DFS is not good choice to find a path in a maze, but it has other applications in finding connected components or maze generation.

A* Search A* uses a greedy search and finds a least-cost path from the given initial node to one goal node out of one or more possibilities. As A* traverses the graph, it follows a path of the lowest expected total cost or distance, keeping a sorted priority queue of alternate path segments along the way. It uses a heuristic cost function of node to determine the order in which the search visits nodes in the graph. For A* we take the first node which has the lowest sum path cost and expected remaining cost. But heuristics must be admissible, that is, it must not overestimate the distance to the goal. The time complexity of A* depends on the heuristic. There are lots of algorithms that run on graphs. I’m going to cover these: Breadth First Search explores equally in all directions. This is an incredibly useful algorithm, not only for regular path finding, but also for procedural map generation, flow field pathfinding, distance maps, and other types of map analysis. Dijkstra’s Algorithm (also called Uniform Cost Search) lets us prioritize which paths to explore. Instead of exploring all possible paths equally, it favors lower cost paths. We can assign lower costs to encourage moving on roads, higher costs to avoid forests, higher costs to discourage going near enemies, and more. When movement costs vary, we use this instead of Breadth First Search. A* is a modification of Dijkstra’s Algorithm that is optimized for a single destination. Dijkstra’s Algorithm can find paths to all locations; A* finds paths to one location, or the closest of several locations. It prioritizes paths that seem to be leading closer to a goal....


Similar Free PDFs