Problem-Solving Agents (Section 3.1)

Goal-based agents for which states of the environment are considered as atomic representations, ones with no visible internal structure.

Formulating the search problem. We assume

  • environment's state is observable
  • environment's state is discrete
  • environment is deterministic
  • environment is sequential
  • environment is static
  • single agent

Problem definition:

  • set of possible states
  • initial state
  • possible actions available at each state
  • transition model, from state, action pair to next state
  • goal test
  • path cost, in this chapter assumed to be sum of step costs

Definition of state set is critical. Want enough detail to enable discovery of useful solution, but want minimal detail to ensure a practical search.

Consider the sliding tile 8-puzzle. Example of

  • too much detail
    • a state includes positions of every tile, orientation in three dimensions of puzzle, time of day, what you had for breakfast, the age of your older sister
  • too little detail
    • state is either "initial" or "solved"

How would you write the problem definition for our simple graph?

  • set of possible states?
  • initial state?
  • possible actions available at each state?
  • goal test?
  • path cost?

How would you write the problem definition for the 8 or 15 puzzle?

  • set of possible states?
  • initial state?
  • possible actions available at each state?
  • goal test?
  • path cost?

How would you write the problem definition for the Towers of Hanoi puzzle?

  • set of possible states?
  • initial state?
  • possible actions available at each state?
  • goal test?
  • path cost?

How would you write the problem definition for the Peg Board Puzzle?

  • set of possible states?
  • initial state?
  • possible actions available at each state?
  • goal test?
  • path cost?

How would you write the problem definition for scheduling 10 different observations using the Hubble Space Telescope?

  • set of possible states?
  • initial state?
  • possible actions available at each state?
  • goal test?
  • path cost?

Ways of searching a graph to find path from start to goal node?

Uninformed search means that the choice of action is not "informed" by any knowledge of the goal.

Breadth-first search completely explores each level of the search space before proceeding to the next.

Depth-first search completely explores a path until it ends, then backs up a level and tries again.

In [2]:
from IPython.display import IFrame
IFrame("https://www.cs.colostate.edu/~anderson/cs440/notebooks/simplegraphsteps.pdf", width=800, height=600)
Out[2]:

This example does not show the reduced space requirements of depth-first search. If the solution path was via the second child of 'a', then we would see that the nodes through the first child of 'a' do not need to be saved.

Here is an algorithm definition for both breadth-first and depth-first search, tailored to fit the python implementation you must complete for Assignment 1. The algorithm maintains a local variable named un_expanded to be a list of nodes whose children have not yet been generated (like the authors' frontier variable), and a dictionary named expanded to keep the nodes for which we have generated the children (like the authors' explored variable). In each a node is stored with its parent, allowing a solution path to be generated be stepping backwards from the goal node once it is found.

Given the start_state, goal_state, successors_f, and breadth_first (a boolean variable):

  • Initialize expanded to be an empty dictionary
  • Initialize un_expanded to be a list containing the pair (start_state, None)
  • If start_state is the goal_state, return the list containing just start_state
  • Repeat the following steps while un_expanded is not empty:
    • Pop from the end of un_expanded a (state, parent) pair.
    • Generate the children of state using the successors_f function.
    • Add tuple(state): parent to the expanded dictionary
    • For efficiency, remove from children any states that are already in expanded or un_expanded. When looking for states in expanded, remember to apply tuple to them first.
    • If the goal has been found (in python, goal_state is in children):
      • Initialize the solution path with the list [state, goal_state].
      • While parent exists:
        • Insert parent to the front of the solution path.
        • Set parent to the parent of parent.
      • Return the solution path.
    • Sort and reverse the list of states in children, so that we all find the same solution paths.
    • Create a modified children list by changing each entry to be a pair (child, parent), where parent is the parent of the child.
    • Insert the modified children list into the un_expanded list at the front if doing breadth-first search, or at the back if doing depth-first search. Use the boolean variable breadth_first provided as the last argument in the call to this function to control inserting at the front the back. Do this insertion with one statement, not a for loop, to preserve the order of the children.

The line that starts with "For efficiency, remove from children any states ..." can be tricky, especially if you try to implement this with a for loop that steps through the elements of children and remove them.

Here is an example of a result that might surprise you.

In [4]:
nums = [0, 6, 4, 5, 2, 3, 9, 7, 8]
nums
Out[4]:
[0, 6, 4, 5, 2, 3, 9, 7, 8]
In [5]:
for num in nums:
    print(num)
0
6
4
5
2
3
9
7
8
In [6]:
for num in nums:
    if num < 5:
        nums.remove(num)

nums
Out[6]:
[6, 5, 3, 9, 7, 8]

Why is 3 still in this list?

This is because you are modifying the list nums which is controlling the for loop. After 2 is removed, the for loop moves on to the value at 9. (Try printing the value of nums at the end of each repetition to see this.)

How can we fix this? There are two ways. The first is to use a copy of the nums list to control the for loop, so you are free to modify the original nums list.

In [7]:
import copy

nums = [0, 6, 4, 5, 2, 3, 9, 7, 8]
for num in copy.copy(nums):
    if num < 5:
        nums.remove(num)
        
nums
Out[7]:
[6, 5, 9, 7, 8]

A second way is to use a list comprehension. This ends up constructing a new list, keeping just the elements you want.

In [10]:
nums = [0, 6, 4, 5, 2, 3, 9, 7, 8]
nums = [num for num in nums if num >= 5]
nums
Out[10]:
[6, 5, 9, 7, 8]