Assignment 1: Uninformed Search

  • A1.1: First paragraph is changed. It no longer mentions a requirement of applying your search functions to a puzzle of your choice.

Type your name here

Breadth-first and depth-first are two algorithms for performing uninformed search---a search that does not use knowledge about the goal of the search. You will implement both search algorithms in python and test them on a simple graph. Then you will apply your search algorithms to the grid navigation problem that you modify as explained below.

Required Code

In this jupyter notebook, you must implement at least the following functions. The first function, search, implements the algorithm shown at the end of Lecture Notes 03 Problem-Solving Agents.

solution_path = search(start_state, goal_state, successors_f, breadth_first)

  • start_state: single state where search starts
  • goal_state: signle state that represents the goal
  • successors_f: function that accepts a single argument that is a state and returns a list of states that can be reached in one step from the argument state
  • breadth_first: True or False. If True, search performs a breadth-first search. Otherwise it performs a depth-first search.
  • solution_path: returned value that is either
    • a list of states that shows the path found from the start state to the goal state, or
    • the string 'Goal not found' if the search has searched everywhere without finding the goal state.

The next two functions are very short, and just call search. These are really just convenience functions so the user of your search algorithm does not need to know what the boolean-valued argument breadth_first means.

solution_path = breadth_first_search(start_state, goal_state, successors_f)

  • start_state: single state where search starts
  • goal_state: signle state that represents the goal
  • successors_f: function that accepts a single argument that is a state and returns a list of states that can be reached in one step from the argument state
  • solution_path: returned value that is either
    • a list of states that shows the path found from the start state to the goal state, or
    • the string 'Goal not found' if the search has searched everywhere without finding the goal state.

solution_path = depth_first_search(start_state, goal_state, successors_f)

  • start_state: single state where search starts
  • goal_state: signle state that represents the goal
  • successors_f: function that accepts a single argument that is a state and returns a list of states that can be reached in one step from the argument state
  • solution_path: returned value that is either
    • a list of states that shows the path found from the start state to the goal state, or
    • the string 'Goal not found' if the search has searched everywhere without finding the goal state.

Each receives as arguments the starting state, the goal state, and a successors function. If they succeed in finding the goal state, breadth_first_search returns the breadth-first solution path as a list of states starting with the start_state and ending with the goal_state. depth_first_search returns the depth-first solution path. If they do not success, they return the string 'Goal not found'.

Test your code by running them with a simple graph as shown in the following example, and with the grid example.

Test your code on other graphs, too. The final grading script will include graphs not shown here.

In [ ]:
def search(start_state, goal_state, successors_f, breadth_first):
    .
    .
    .
In [ ]:
def breadth_first_search(start_state, goal_state, successors_f):
    .
    .
    .
In [ ]:
def depth_first_search(start_state, goal_state, successors_f):
    .
    .
    .

Example 1

Here is a simple example. States are defined by lower case letters. A dictionary stores a list of successor states for each state in the graph that has successors.

In [1]:
successors = {'a':  ['b', 'c', 'd'],
              'b':  ['e', 'f', 'g'],
              'c':  ['a', 'h', 'i'],
              'd':  ['j', 'z'],
              'e':  ['a', 'k', 'l'],   # Watch out.  This creates the cycle a -> b -> e-> a
              'g':  ['m'],
              'k':  ['z']}
successors
Out[1]:
{'a': ['b', 'c', 'd'],
 'b': ['e', 'f', 'g'],
 'c': ['a', 'h', 'i'],
 'd': ['j', 'z'],
 'e': ['a', 'k', 'l'],
 'g': ['m'],
 'k': ['z']}

Here is an example of a successors function that works for any search problem whose graph is explicitly represented with a successors dictionary as used in this example.

In [3]:
def successors_f(state): 
    successors = {'a':  ['b', 'c', 'd'],
                  'b':  ['e', 'f', 'g'],
                  'c':  ['a', 'h', 'i'],
                  'd':  ['j', 'z'],
                  'e':  ['a', 'k', 'l'],   # Watch out.  This creates the cycle a -> b -> e-> a
                  'g':  ['m'],
                  'k':  ['z']}
    return successors.get(state, [])
In [4]:
successors_f('a')
Out[4]:
['b', 'c', 'd']
In [5]:
successors_f('e')
Out[5]:
['a', 'k', 'l']
In [6]:
successors_f('q')
Out[6]:
[]
In [7]:
breadth_first_search('a', 'a', successors_f)
Out[7]:
['a']
In [8]:
breadth_first_search('a', 'b', successors_f)
Out[8]:
['a', 'b']
In [9]:
breadth_first_search('a', 'c', successors_f)
Out[9]:
['a', 'c']
In [10]:
breadth_first_search('a', 'd', successors_f)
Out[10]:
['a', 'd']
In [11]:
breadth_first_search('a', 'e', successors_f)
Out[11]:
['a', 'b', 'e']
In [12]:
breadth_first_search('a', 'm', successors_f)
Out[12]:
['a', 'b', 'g', 'm']
In [13]:
for goal in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'z']:
    path = breadth_first_search('a', goal, successors_f)
    print(f'Path from a to {goal}: {path}')
Path from a to a: ['a']
Path from a to b: ['a', 'b']
Path from a to c: ['a', 'c']
Path from a to d: ['a', 'd']
Path from a to e: ['a', 'b', 'e']
Path from a to f: ['a', 'b', 'f']
Path from a to g: ['a', 'b', 'g']
Path from a to h: ['a', 'c', 'h']
Path from a to i: ['a', 'c', 'i']
Path from a to j: ['a', 'd', 'j']
Path from a to k: ['a', 'b', 'e', 'k']
Path from a to l: ['a', 'b', 'e', 'l']
Path from a to m: ['a', 'b', 'g', 'm']
Path from a to z: ['a', 'd', 'z']
In [14]:
for goal in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'z']:
    path = depth_first_search('a', goal, successors_f)
    print(f'Path from a to {goal}: {path}')
Path from a to a: ['a']
Path from a to b: ['a', 'b']
Path from a to c: ['a', 'c']
Path from a to d: ['a', 'd']
Path from a to e: ['a', 'b', 'e']
Path from a to f: ['a', 'b', 'f']
Path from a to g: ['a', 'b', 'g']
Path from a to h: ['a', 'c', 'h']
Path from a to i: ['a', 'c', 'i']
Path from a to j: ['a', 'd', 'j']
Path from a to k: ['a', 'b', 'e', 'k']
Path from a to l: ['a', 'b', 'e', 'l']
Path from a to m: ['a', 'b', 'g', 'm']
Path from a to z: ['a', 'b', 'e', 'k', 'z']

Example 2

Let's try a navigation problem around a grid of size 10 x 10. Rows and columns will be indexed from 0 to 9.

The following function takes the input state and returns all possible states.

In [15]:
def grid_successors(state):
    row, col = state
    # succs will be list of tuples () rather than list of lists [] because state must
    # be an immutable type to serve as a key in dictionary of expanded nodes
    succs = []
    for r in [-1, 0, 1]:   #check each row
        for c in [-1, 0, 1]:  # check in each col
            newr = row + r
            newc = col + c
            if 0 <= newr <= 9 and 0 <= newc <= 9:  
                succs.append( (newr, newc) )
    return succs
In [16]:
grid_successors([3,4])
Out[16]:
[(2, 3), (2, 4), (2, 5), (3, 3), (3, 4), (3, 5), (4, 3), (4, 4), (4, 5)]
In [17]:
grid_successors([3,9])
Out[17]:
[(2, 8), (2, 9), (3, 8), (3, 9), (4, 8), (4, 9)]
In [18]:
grid_successors([0,0])
Out[18]:
[(0, 0), (0, 1), (1, 0), (1, 1)]
In [19]:
print('Breadth first')
print('path from (0, 0) to (9, 9) is', breadth_first_search((0, 0), (9, 9), grid_successors))
Breadth first
path from (0, 0) to (9, 9) is [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]
In [20]:
print('Depth-first')
print('path from (0, 0) to (9, 9) is', depth_first_search((0, 0), (9, 9), grid_successors))
Depth-first
path from (0, 0) to (9, 9) is [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (1, 9), (2, 8), (2, 7), (2, 6), (2, 5), (2, 4), (2, 3), (2, 2), (2, 1), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0), (8, 0), (9, 1), (8, 2), (7, 2), (6, 2), (5, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (5, 9), (6, 8), (6, 7), (6, 6), (6, 5), (7, 4), (8, 4), (9, 5), (8, 6), (8, 7), (8, 8), (9, 9)]

Let's plot the paths.

In [21]:
path_dfs = depth_first_search((0, 0), (9, 9), grid_successors)
path_dfs
Out[21]:
[(0, 0),
 (0, 1),
 (0, 2),
 (0, 3),
 (0, 4),
 (0, 5),
 (0, 6),
 (0, 7),
 (0, 8),
 (1, 9),
 (2, 8),
 (2, 7),
 (2, 6),
 (2, 5),
 (2, 4),
 (2, 3),
 (2, 2),
 (2, 1),
 (3, 0),
 (4, 0),
 (5, 0),
 (6, 0),
 (7, 0),
 (8, 0),
 (9, 1),
 (8, 2),
 (7, 2),
 (6, 2),
 (5, 2),
 (4, 3),
 (4, 4),
 (4, 5),
 (4, 6),
 (4, 7),
 (4, 8),
 (5, 9),
 (6, 8),
 (6, 7),
 (6, 6),
 (6, 5),
 (7, 4),
 (8, 4),
 (9, 5),
 (8, 6),
 (8, 7),
 (8, 8),
 (9, 9)]

Now, we have the path to goal state. To plot this path we must extract the first value in each tuple and put them in a list called rows to use as the $y$ coordinate of each point, and build a second list called cols of second values.

In [22]:
import matplotlib.pyplot as plt
In [23]:
rows = [location[0] for location in path_dfs]
cols = [location[1] for location in path_dfs]
plt.plot(rows, cols, 'o-');
In [24]:
path_bfs = breadth_first_search((0, 0), (9, 9), grid_successors)
print(path_bfs)
rows = [location[0] for location in path_bfs]
cols = [location[1] for location in path_bfs]
plt.plot(rows, cols, 'o-');
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)]
In [25]:
depth_first_search((0, 0), (9, 20), grid_successors)
Out[25]:
'Goal not found'

Required Modification to the Grid Puzzle (worth 20 points)

Define a new function named grid_successors_center_block by copying the above grid_successors function and then modify it to add a square obstacle from coordinates (4, 4) to (6, 6). Plot the path that results from doing breadth-first and depth-first searches with the start and goal states shown above. Insert code cells and markdown cells here to do these steps.

Define grid_successors_center_block here for 5 points.

Replace this text with at least two sentences of text describing changes you made to grid_successors to implement grid_successors_center_block for 5 points.

Make plots of paths resulting from breadth-first and depth-first searches of the grid for 5 points.

Replace this text with at least five sentences of text describing your resulting plots. How are they different with the center block? for 5 points.

Extra Credit

For extra credit, use your functions to solve the Camels Puzzle, described at Logic Puzzles. The following code illustrates one possible state representation and shows results of a breadth-first and a dept-first search. You must define a new successors function, called camel_successors_f.

If you do not do this extra credit step, please remove this and the following cells that pertain to this extra credit.

In [26]:
camel_start_state
Out[26]:
['R', 'R', 'R', 'R', ' ', 'L', 'L', 'L', 'L']
In [27]:
camel_goal_state
Out[27]:
['L', 'L', 'L', 'L', ' ', 'R', 'R', 'R', 'R']
In [28]:
camel_successors_f(camel_start_state)
Out[28]:
[['R', 'R', 'R', ' ', 'R', 'L', 'L', 'L', 'L'],
 ['R', 'R', 'R', 'R', 'L', ' ', 'L', 'L', 'L']]

A handy function for implementing camel_successors_f is index that returns the index where an element is found in a list or a tuple.

In [29]:
camel_start_state.index(' ')
Out[29]:
4
In [30]:
children = camel_successors_f(camel_start_state)
print(children[0]) 
camel_successors_f(children[0])
['R', 'R', 'R', ' ', 'R', 'L', 'L', 'L', 'L']
Out[30]:
[['R', 'R', ' ', 'R', 'R', 'L', 'L', 'L', 'L'],
 ['R', 'R', 'R', 'L', 'R', ' ', 'L', 'L', 'L']]
In [31]:
def print_camel_state(state):
    return ''.join(state)

print_camel_state(camel_start_state)
Out[31]:
'RRRR LLLL'
In [32]:
bfs = breadth_first_search(camel_start_state, camel_goal_state, camel_successors_f)
print(f'Breadth-first solution: ({len(bfs)} steps)')
for s in bfs:
    print(print_camel_state(s))

dfs = depth_first_search(camel_start_state, camel_goal_state, camel_successors_f)
print(f'Depth-first solution: ({len(dfs)} steps)')
for s in dfs:
     print(print_camel_state(s))
Breadth-first solution: (25 steps)
RRRR LLLL
RRR RLLLL
RRRLR LLL
RRRLRL LL
RRRL LRLL
RR LRLRLL
R RLRLRLL
RLR RLRLL
RLRLR RLL
RLRLRLR L
RLRLRLRL 
RLRLRL LR
RLRL LRLR
RL LRLRLR
 LRLRLRLR
L RLRLRLR
LLR RLRLR
LLRLR RLR
LLRLRLR R
LLRLRL RR
LLRL LRRR
LL LRLRRR
LLL RLRRR
LLLLR RRR
LLLL RRRR
Depth-first solution: (25 steps)
RRRR LLLL
RRR RLLLL
RRRLR LLL
RRRLRL LL
RRRL LRLL
RR LRLRLL
R RLRLRLL
RLR RLRLL
RLRLR RLL
RLRLRLR L
RLRLRLRL 
RLRLRL LR
RLRL LRLR
RL LRLRLR
 LRLRLRLR
L RLRLRLR
LLR RLRLR
LLRLR RLR
LLRLRLR R
LLRLRL RR
LLRL LRRR
LL LRLRRR
LLL RLRRR
LLLLR RRR
LLLL RRRR

Grading

Your notebook will be run and graded automatically. Download A1grader.tar and extract A1grader.py from it. Run the code in the following cell to demonstrate an example grading session. You should see a perfect score of 80/80 if your functions are defined correctly.

The remaining 20% will be based on your writing. In markdown cells, explain what your functions are doing and make observations about your results. Also mention problems you encountered in trying to solve this assignment.

Check-in

Do not include this section in your notebook.

Name your notebook Lastname-A1.ipynb. So, for me it would be Anderson-A1.ipynb. Submit the file using the Assignment 1 link on Canvas.

In [41]:
%run -i A1grader.py
======================= Code Execution =======================

Extracting python code from notebook named 'Anderson-A1.ipynb' and storing in notebookcode.py
Removing all statements that are not function or class defs or import statements.

Searching this graph:
 {'a': ['b'], 'b': ['c', 'd'], 'c': ['e'], 'd': ['f', 'i'], 'e': ['g', 'h', 'i']}

Looking for path from a to b.
  Calling breadth_first_search(a, b, successorsf)
       and depth_first_search(a, b, successorsf)

10/10 points. Your breadth_first_search found correct solution path of ['a', 'b']
10/10 points. Your depth_first_search found correct solution path of ['a', 'b']

Looking for path from a to i.
  Calling breadth_first_search(a, i, successorsf)
      and depth_first_search(a, i, successorsf)

20/20 points. Your breadth_first_search found correct solution path of ['a', 'b', 'd', 'i']
20/20 points. Your depth_first_search found correct solution path of ['a', 'b', 'c', 'e', 'i']

Looking for nonexistent path from a to denver.
  Calling breadth_first_search(a, denver, successorsf)
      and depth_first_search(a, denver, successorsf)

10/10 points. Your breadth_first_search found correct solution path of Goal not found
10/10 points. Your depth_first_search found correct solution path of Goal not found

======================================================================
notebooks Execution Grade is 80 / 80
======================================================================

__ / 5 points. Correct implementation of the grid_successors_center_block function

__ / 5 points. At least two sentences of Text describing changes you made to grid_successors to implement grid_successors_center_block

__ / 5 points. Plots of paths resulting from breadth-first and depth-first searches of the grid

__ / 5 points. At least five sentences of text describing your resulting plots.  How are they different with the center block?

======================================================================
notebooks Discussion Grade is __ / 20
======================================================================

======================================================================
notebooks FINAL GRADE is  _  / 100
======================================================================

Extra Credit: Earn one point of extra credit for using your search functions to solve the camel puzzle.

notebooks EXTRA CREDIT is 0 / 1