Introduction

Welcome to CMSI 282: your window into the wonderful world of algorithms!


Course Outline


For those of you viewing from home, at this point we'll review the class syllabus, located here:

 Syllabus


Site Structure


Before we get started, some things to note about these course notes and the site you're currently viewing:

  • You can add notes inside the website so that you can follow along and type as I say stuff! Just hit SHIFT + N and then click on a paragraph to add an editable note area below. NOTE: the notes you add will not persist if you close your browser, so make sure you save it to PDF when you're done taking notes! (see below) Having said that, know that there is a lot of research that suggests taking hand-written notes to be far more effective at memory retention than typing them.

  • The site has been optimized for printing, which includes the notes that you add, above. I've added a print button to the bottom of the site, but really it just calls your printer functionality, which typically includes an option to export to PDF.

  • The course notes listed herein will have much of the lecture content, but not all of it; you are responsible for knowing the material you missed if you are absent from a lecture.

Any time you see a blue box with an information symbol (), this tidbit is a definition, factoid, or just something I want to draw your attention to; typically these will help you with the conceptual portions of the homework assignments.

Any time you see an orange box with a cog symbol (), this tidbit is a useful tool in your programming arsenal; typically these will help you with the concrete portions of your homework assignments.

Any time you see a red box with a warning symbol (), this tidbit is a warning for a common pitfall; typically these will help you with debugging or avoiding mistakes in your code.

Any time you see a yellow question box on the site, you can click on it...

...to reveal the answer! Well done!


With that said, let's jump right into it!



Problem Solving

Since this course is about algorithms, let's start by making sure we're all on the same page!


Forward


What is an algorithm?

A formalized procedure for accomplishing some task.

Pretty basic! In fact, you might argue that everything you've been studying in the major thus far has been a study of algorithms -- and you'd be right!

So what makes this class different?

Herein, we study a variety of algorithm paradigms that provide prototypical algorithm formats that can be applied to a wide variety of applicable tasks.

These paradigms generally have a focus on solving large problems that would be computationally intractible without doing something clever -- the heart of computer science!

With that said, let's look at our first paradigm with regards to generalized problem solving: it's all about search.


Search

Combinatorial Search (or just Search for short) is the study of algorithms that attempt to solve problems with a large number of potential solutions.

Algorithms that procedurally *find* a solution from amongst these possibilities will be at the center of this first portion of the class!

Some areas of search belong to the study of artificial intelligence, but can be applied to so many other tasks that we will give this topic a solid treatment in 282.

The process of problem solving can be condensed to three steps:

  1. Formalize the problem

  2. Search for a solution (reached through some sequence of actions)

  3. Execute that plan of action

Just how we define the problem, environment, and actions available to our search algorithms will be of first concern for us.

So, let's start with a motivating example problem, and then see how we can formalize it to be amenable for search.


Motivating Example


Let's take a look at a classic Grid World, where our agent starts at some position on a grid, and our job is to get them to some goal cell.

If our agent can move Up, Down, Left, or Right, what are some solutions to this problem?

Let's see how to formalize this problem, and then look at search strategies.


Formally, a problem consists of 5 specifications, detailed below (see Roman Numerals, I, II, III, ...).

States are a given instantiation of a search problem's variables of interest, which are liable to change as the agent acts.

I. An initial state is the state at the start of the problem.

II. A goal test / state determines if the state represents a solution to the problem.

An intermediary state is a state between the initial and goal states, as would be encountered during search.

What might a state look like in our motivating problem? What would the initial and goal states look like?

How about the agent's position on the board? The initial state is the cell they begin within (1, 1) and the goal is the position of the goal (1, 3).


III. Actions are the set of choices available to the agent at a state \(s\) such that $$Actions(s) = \{ a_1, a_2, ... \}$$

What can our agent do to manipulate the state? When is are some actions applicable and when are they not?

Move Left, Right, Up, or Down! Not applicable when such a movement would move them into a barrier.


IV. Transition Model specifies how action \(a\) taken by the agent in state \(s\) changes the state to \(s'\), or formally:
$$Result(s, a) = s'$$

This makes sense since, if our state is the agent's position, then when they move, they modify the current state of the problem.

How does an action manipulate / transition the state in our example?

Without loss of generality, Move((x, y), "Up") = (x, y+1), etc.

The set of all possible states that can be reached from the initial state via valid actions is called the state space.

What is the state space in the problem above?

All of the open tiles, including the goal!


V. Costs specify the cost of a particular transition such that for some cost \(c\), $$Cost(s, a) = c$$

What would be a reasonable cost to associate with each of our transitions in the example problem?

Associate a cost of 1 for each valid movement.

When every transition in the problem has the same cost, we call this the uniform cost assumption



Evaluating Searches

Now that we've defined a problem, we need a means of actually using our carefully laid-out definitions!

Search can be thought of as a function that takes in a problem specification, and returns a sequence of actions that transition the initial state into the goal state, if it exists: $$Search(problem) = solution$$

In our motivating Maze Pathfinding example, what would a solution look like?

A series (i.e., ordered sequence) of actions that lead from the initial to a goal state: $$solution = [a_1, a_2, ..., a_k] = ["U", "D", ...]$$

Search strategies define a procedural means of finding such a satisfying solution from a formalized problem.

In the context of Maze Pathfinding, we'll need our search strategies to:

  • Explore all possible transitions that might lead us to the goal from the initial state.

  • Remember the paths we've explored along the way.

Can you think of an appropriate data structure that might be well suited for this purpose? Describe its properties.

A tree where the initial state is at the root (or a graph, for reasons we'll discuss later).

A search tree is a tree with the initial state at the root, transitions along edges, and nodes corresponding to states that can be reached from one action applied to the parent.

If a goal is reached through some path from the root, then a solution exists, and the given path can be executed.

Consider a haphazard search tree (strategy omitted, for now) that corresponds to the following actions:

This is certainly a solution, but what's wrong with it?

It's not optimal! There's a shorter route available. Formally, we say that this solution does not minimize cost.

So, naturally, we should first describe what properties of a solution a search strategy should optimize, and then consider some candidate algorithms:


Metrics of Optimality


There are two primary metrics of optimality for a search strategy that relate to the problem itself.

Related to the problem definition, what two properties should be true of any solution returned by a good search strategy?

Completeness: if a solution exists, the strategy will find it.

Optimal Cost: if an optimal solution exists, the strategy will not return a sub-optimal one.


There are also two classic metrics of optimality from a computational standpoint:

What computational metrics of success should we analyze for any given search strategy?

Space and Time Complexity

What do we generally mean when we discuss an algorithm's time and space complexity?

Some sort of asymptotic bound for the growth of the amount of time the algorithm takes to complete, or the memory that must be allocated to it as a function of the size of the input (or in our case, the search space).


With these metrics of success in place, let's look at some basic search strategies:



Uninformed Search

Uninformed search strategies proceduralize search by the order in which nodes are generated in the search tree.

Search tree generation happens in two primary stages:

Expanding a node is akin to visiting it on a putative solution path, which then generates all child nodes from legal actions that could lead from it.

Because we are interested in returning a sequence of actions (i.e., a path) for a solution in the search tree, what should each node remember?

Each node in a search tree tracks: (1) the state it represents, (2) the parent that generated it, (3) the action that generated it, (4) the path cost \(g(n)\) encountered up until that point.


Here is a search tree where we expand the root, generating its children (assuming each movement is legal), and then expand the child node corresponding to a taken "right" action:

Note: arrows shown below indicate the generation order (parent -> child), but recall from above that references are stored from child to parent.

Problem: there are invalid states / nodes generated in this graph from our sample problem -- so only generate those that are valid transitions!

Show the first two node expansions that would begin construction of the search tree in our example problem:

In general, uninformed searches are distinguished by the order they expand nodes, which we can keep track of in what is known as the tree's frontier:

The set of all leaf nodes available for expansion at any given point of a search is called the frontier (AKA open-list).

Let's look at our first uninformed search now...



Breadth-First Search

We all remember BFS from data structures, yes? Well it's back to haunt us!

What were the characteristics of breadth-first search, and what data structure did we use to accomplish it?

BFS searches "level by level" in a search tree, never expanding a node that is more than 1 level deeper than another. We used a Queue to implement it.

BFS: (FIFO) Enqueue newly generated nodes in the frontier, and expand in dequeued order.

Use BFS to find a solution to our original problem (omit some paths that don't lead to the solution, for ease). Consider that child nodes are generated using the action precedence: Right, Down, Left, Up


Breadth-first Search Optimality


Let's stop to consider some of our search strategy optimality metrics.

Is BFS complete?

Yes! It is guaranteed to find a solution if one exists, since it will never generate a level that is more than 1 depth greater than the last.

Is BFS optimal?

Yes, for uniform-cost problems, because all states on the same depth will share the same cost, and it will find the most shallow goal state.


Now, to answer questions of computational and space complexity, we need some extra descriptives for the search trees we are generating.

Describe some of the important characteristics of a search tree for time and space complexity.

A search tree's branching factor (b) of a search strategy is the maximum number of children a node can generate when expanded.

A search tree's depth (d) is the depth of the most shallow goal node.

Consider the example tree below; what are its values for \(b, d\)?


Let's now consider the time complexity / assymptotic runtime efficiency in terms of \(b, d\); consider:

[Worst Case] How many nodes must be expanded at depth 0? Depth 1? Depth 2? ... Depth d?

So, what is the asymptotic time complexity of BFS?

\(O(b^0 + b^1 + b^2 + ... + b^d) = O(b^d)\)

Since we're incrementally building our search tree, we need to hold nodes in memory, so...

[Worst Case] How many nodes do we need to keep in memory at depth 0? Depth 1? Depth 2? ... Depth d?

So, what is the asymptotic space complexity of BFS?

\(O(b^0 + b^1 + b^2 + ... + b^d) = O(b^d)\)


Visualization


Let's take a second to visualize what's happening with our BFS; here's a cool visualization tool:

Search Visualization



Memoization

Observant students will look at the last search tree and remark: some states are repeated in expansion!

This is certainly a computationally costly fault with search-trees. Certainly we can do better.

Will breadth-first search's completeness be compromised by repeated states?

No, because even though some paths are re-explored, any path that eventually leads to a solution will still be discovered because BFS explores along each path 1 action at a time.

Can you suggest an alternative approach that would avoid repeated states?

Search graphs are exactly like search trees, except that search using a graph maintains a closed set / graveyard of previously expanded states, and only those states that are not in the closed list are ever added to the frontier.

Take, for example, our Maze Pathfinding problem wherein we can compare Tree Search (below, left) to Graph Search (below, right) in a BFS exemplified below.

Note: repeated states that have already been expanded need not proliferate the expansion of the tree, as is indicated in the purple edges in the graph that return the search to a previously expanded state.

The programming paradigm of remembering previously computed *subproblems* in pursuit of finding the solution to a larger problem is known as memoization (AKA caching), and is useful in reducing computational overhead a wide swath of problems.

Although called "Graph Search", the graphical component is largely conceptual -- in practice, the maintenance of knowing which states have already been visited can be accomplished through use of a companion data structure.

What data structure would well suit the task of memoizing which states have already been expanded in graph search?

A Hash Table (specifically, a Set), since arbitrary memoization and lookup can be accomplished in \(O(1)\).


Be sure to memoize memoization -- it's an incredibly useful tool to remember!

Of course, we've only looked at BFS for our uninformed search strategies; it's time to examine the merits of another approach...



Depth-First Search

Naturally, if BFS is an uninformed search strategy, we should examine the merits of Depth-First Search as well!

What were the characteristics of depth-first search, and what data structure did we use to accomplish it?

DFS prioritizes "deepest" nodes first in a search tree, always expanding the nodes at the greatest depth first. We used a Stack to implement it in data-structures.

DFS: (FILO) Push newly generated nodes onto the stack frontier, and expand in popped order.

Attempt to use DFS on our example problem; what divergent cases can happen?

Oh no! We're stuck in an endless expansion loop!

What constraints could we put on our DFS to avoid this scenario?

(1) Use memoization to avoid repeated states, or (2) limit the depth it could expand to.

It might seem like approach (1) is the obvious solution to our problem, but perhaps there's some merit to considering solution #2 as well -- let's take a look!


Depth-limited Search


In depth-limited search, we set some depth threshold \(m\) such that the search can never expand nodes past a depth of \(m\).

Using depth-limited search, suppose we generated the following abstract search tree \((b = 3)\):

What is the value of \(m\) in the above?

[Worst Case] How many nodes must be expanded at depth 0? Depth 1? Depth 2? ... Depth m?

So, what is the asymptotic time complexity of Depth-Limited Search?

\(O(b^0 + b^1 + b^2 + ... + b^m) = O(b^m)\)

Nothing shocking there, but now let's consider: what about the space complexity?

Consider: what happens when we perform expansion #6 in our tree above? (demonstrated below)

We can free the memory associated with any path that did not lead to a solution!

As such, the space-complexity of depth-limited search is \(O(b*m)\), an improvement over BFS.

However, there's a problem with depth-limited search as well... what if we don't know \(m\)?

Depth-limited search may choose an \(m < d\), the solution depth, and so it is not complete!

Is there a remedy to this problem?

Iteratively increase the depth limit until a goal is found!


Iteratively-Deepening DFS (IDDFS)


Iteratively Deepening DFS (IDDFS) performs depth-limited search for \(m = 1, m = 2, ..., m = d\) (i.e., until the limit is the same as the goal depth).

Visually, this looks like a depth limit \(L\) expanding until a goal is found:


IDDFS thus preserves the completeness enjoyed by BFS while gaining the space-saving features of DFS.

Will IDDFS have the same time complexity as BFS? Why or why not?

Yes! Because the largest depth explored, \(O(b^L)\) will always dominate the other, smaller search trees explored (asymptotically).

As such, BFS has a smaller constant-of-proportionality due to the IDDFS overhead of exploring smaller models, but has the same asymptotic runtime.


That brings us to the end of our "dumb" uninformed searches! Next week, we look at... you guessed it... searches that know what's up!



  PDF / Print