Chapter 3
Solving Problems by Searching  

powered by FreeFind

Modified: 

Outline

3.1 PROBLEM-SOLVING AGENTS

Well-defined problems and solutions

Problem has 4 components:

  1. Initial state of agent

    In(Arad)

     

  2. Possible actions often implemented as a successor function.

    successor( In(Arad) ) returns

    { <Go(Sibiu), In(Sibiu)>,
       <Go(Timisoara), In(Timisoara)>,   
       <Go(Zerind), In(Zerind)>
    }

  3. Goal test determines if a state is the goal.

    In (Bucharest)

     

  4. Path cost assigns numeric cost to each path to allow determining best. Possible metrics are: distance, time, etc.

3.2 EXAMPLE PROBLEMS

Vacuum world

 

 

 

 

 

 

 

 

 

 

State space representation


(Location, A Status, B Status) =>
                                                [ (Action, (Location, A Status, B Status)),
                                                   ...

Example:

{ (A, Dirty, Dirty)  : [ (Suck, (A, Clean, Dirty)),
                                (Right, (B, Dirty,  Dirty)),
                                (Left,   (A, Dirty,  Dirty)) ],
   (A, Clean, Dirty) : ...

 

 

3.3 SEARCHING FOR SOLUTIONS

TREE-SEARCH

Basic idea - Offline exploration of search space by generating successors of already explored states

 

Example

Problem - Arad to Bucharest.

Strategy - Examine connected cities in alphabetical order.

 

 

 

Measuring problem-solving performance

 

3.4 UNIFORMED SEARCH STRATEGIES

Breadth-first search

fringe = [ A ]

 

fringe = [ B, C ]

fringe = [ C, D, E ]

 


fringe = [ D, E, F, G ]

 

Properties of breath-first search

d - depth, number of steps along the path from the initial state to goal

b - maximum branching factor

 

 

Depth-first search

Goal: M

fringe = [ A ]

 

fringe = [ B, C ]

fringe = [ D, E, C ]


fringe = [ H, I, E, C ]

 

fringe = [ I, E, C ]

fringe = [ E, C ]

 

fringe = [ J, K, C ]

 

fringe = [ K, C ]

fringe = [ C ]

 

fringe = [ F, G ]

fringe = [ L, M, G ]

fringe = [ M, G ]

 

Properties of depth-first search

d - depth, number of steps along the path from the initial state to goal

b - branching factor

m - maximum depth

 

IMPLEMENTATION: GENERAL TREE-SEARCH (Figure 3.9)

Python: Path cost and action have been ignored for now for clarity.

def TREE_SEARCH(problem, fringe):
    fringe = INSERT( MAKE_NODE(INITIAL_STATE[problem]), fringe)
    while True:
        node = REMOVE_FIRST(fringe)
        if GOAL_TEST[problem] == node.STATE : return SOLUTION(node)
        fringe = INSERT_ALL( EXPAND(node, problem), fringe)
 
def EXPAND( node, problem ) :
    successors = []
    for result in SUCCESSOR_FN[ problem ]( node.STATE ) :
        s = Node(node)
        s.STATE = result
        s.PARENT_NODE = node
        successors = INSERT(s, successors)
    return successors

Example

Given the tree at right, find a path to the goal of reaching J.

PROBLEM='Go to J'
INITIAL_STATE = { 'Go to J':'A'}
GOAL_TEST = { 'Go to J':'J'}

 

#   Figure 3.9 p. 72

def TREE_SEARCH(problem, fringe):
    fringe = INSERT( MAKE_NODE(INITIAL_STATE[problem]), fringe)
    while True:
        node = REMOVE_FIRST(fringe)
        if GOAL_TEST[problem] == node.STATE : return SOLUTION(node)
        fringe = INSERT_ALL( EXPAND(node, problem), fringe)
       
def EXPAND( node, problem ) :
    successors = []
    for result in SUCCESSOR_FN[problem](node.STATE) :
        s = Node(node)                                 # create node for each in state list
        s.STATE = result                               # e.g. result = 'F' then 'G' from list ['F', 'G']
        s.PARENT_NODE = node
        s.DEPTH = node.DEPTH+1
        successors = INSERT(s, successors)
    return successors

def successor_fn( state ) :                           # Lookup list of successor states
    return STATE_SPACE[ state ]                  #  successor_fn( 'C' ) returns ['F', 'G']
                                                                   
PROBLEM='Go to J'
INITIAL_STATE = { 'Go to J':'A'}
GOAL_TEST = { 'Go to J':'J'}
SUCCESSOR_FN = {'Go to J': successor_fn } # successor_fn is a function

STATE_SPACE = { 'A' : ['B', 'C'],
#                        /              \
            'B' : ['D', 'E'],         'C' : ['F', 'G'],
#               /         \               /         \
        'D' : [],     'E' : [],     'F' : [],     'G' : ['H','I','J'],
#                                                         /     |     \
                                                       'H':[], 'I':[], 'J':[], }

def MAKE_NODE(state) :
    return Node(state)

def INSERT(node, queue) :           # Insert at front
    queue[:0] = [node]
    return queue

def INSERT_ALL( list, queue ) :
    for node in list :
        INSERT(node, queue)
    return queue

def EMPTY( queue ) :
    return len(queue) == 0

def REMOVE_FIRST( queue ) :
    if len(queue) != 0 :
        first = queue[0]
        queue[0:1]=[]
        return first
    return []

def SOLUTION(node) :
    return node.path()

def run() :
    print 'Solution path'
    for node in TREE_SEARCH( PROBLEM, []) :
        node.display()

#__________________________________________________________
class Node:                      # Node has only PARENT_NODE, STATE, DEPTH
    def __init__(self, state, parent=None, depth=0, action=None):
        self.STATE = state
        self.PARENT_NODE = parent
        self.DEPTH = depth

    def path(self):               # Create a list of nodes from the root to this node.
        x, result = self, [self]
        while x.PARENT_NODE:
            result.append(x.PARENT_NODE)
            x = x.PARENT_NODE
        return result

    def display(self) :
         print self.STATE, '  ', self.DEPTH,
 

 

The complete search tree at right is produced by the state space:

STATE_SPACE = {
                  'A' : ['B', 'C'],
#                        /      \
            'B' : ['D', 'E'],  'C' : ['F', 'G'],
#                  /        \           /       \
            'D' : [],  'E' : [],  'F' : [],  'G' : ['H','I','J'],
#                                                      /     |     \
                                                  'H':[], 'I':[], 'J':[], }

def TREE_SEARCH(problem, fringe):
    fringe = INSERT( MAKE_NODE(INITIAL_STATE[problem]), fringe)
    while True:
        node = REMOVE_FIRST(fringe)
        if GOAL_TEST[problem] == node.STATE :
                                                 return SOLUTION(node)
        fringe = INSERT_ALL( EXPAND(node, problem), fringe)
       
def EXPAND( node, problem ) :
    successors = []
    for result in SUCCESSOR_FN[problem](node.STATE) :
        s = Node(node)                                 # create node for each in state list
        s.STATE = result                               # e.g. result = 'F' then 'G' from list ['F', 'G']
        s.PARENT_NODE = node
        s.DEPTH = node.DEPTH+1
        successors = INSERT(s, successors)
    return successors

def successor_fn( state ) :                           # Lookup list of successor states
    return STATE_SPACE[ state ]                  #  successor_fn( 'C' ) returns ['F', 'G']
  1. Successor nodes are inserted at front of the fringe (successor list) as a node is expanded. Is this a breadth (LIFO) or depth-first search (FIFO)?
  2. For goal J, give the fringe (successor list) after expanding each node.
  3. What is the effect of inserting successor nodes at the end of the fringe as a node is expanded? A depth or breadth-first search?
  4. For goal J, give the fringe (successor list) after expanding each node.

 

Example: N-queens problem (Download)

N > 3 queens on chess board in non-attacking positions (no two queens on same row, column, or diagonal).

Backtracking

In the figure at right, an attempt to place queen in last row of column 2 fails. The queen in column 2, row 3 and the predecessor column must be removed; the predecessor queen must be relocated in another non-conflicting row (see figure below). This strategy is generally known as backtracking.

Recall the successor_fn generates all states with non-conflicting rows for the left-most empty column. When the last row is reached, no non-conflicting rows remain; the successor_fn returns an empty list of states. The first state in the LIFO fringe is of a previous column producing the backtracking effect to the predecessor column.

For example, the first state in the fringe below would be removed for expansion which results in an empty list of non-conflicting rows:

[ [0, 3, 1, None],
  [1, None, None, None], [2, None, None, None], [3, None, None, None] ]

due to the following statement returning [] .

      return [(row, place(col, row)) for row in range(N)
                                                      if not conflicted(state, row, col)]

The fringe is then:

  [[1, None, None, None], [2, None, None, None], [3, None, None, None] ]

creating the effect of backtracking to column 0 with state:

 [1, None, None, None]

for expansion.

  1. What is the result of successor_fn( [1, None, None, None])?
  2. What is the number of possible states for placing one queen on an empty 4x4 board?
  3. What is the number of possible states on a 4x4 board after placing 1 queen?
  4. What are the total number of possible states for the 4-queens problem?
  5. Is this a depth or breadth-first search?
  6. What happens in TREE-SEARCH if there are no solutions?

 

def successor_fn(state):
   global N                # In leftmost empty column, try all non-conflicting rows.
   if state[-1] is not None:
      return ['Solution']                  # All columns filled; no successors
   else:
      def place(col, row):
         new = state[:]                    # Make a copy of state
         new[col] = row
         return new

      col = state.index(None)

      return [ place(col, row) for row in range(N) if not conflicted(state, row, col) ]
                                                   # backtrack when [] non-conflicting states

def conflicted(state, row, col):        # Queen at (row, col) conflict?
   for c in range(col):
      if conflict(row, col, state[c], c):
         return True
   return False

def conflict(row1, col1, row2, col2): # Queens in (row1, col1) and (row2, col2) conflict?
   return (row1==row2 or col1==col2 or row1-col1==row2-col2 or row1+col1==row2+col2)
# same row, column or diagonal (| - \/) ?

PROBLEM='N Queens'
GOAL_TEST = { 'N Queens': 'Solution'}
SUCCESSOR_FN = {'N Queens': successor_fn } # successor_fn is a function
INITIAL_STATE = {}

def run(n) :
   global N, INITIAL_STATE
   N=n
   INITIAL_STATE = { 'N Queens': [None]*n}
   for node in TREE_SEARCH( PROBLEM, []) :
      node.display()                                            # print 'Solution path'

 

Uniform-cost search

(or Branch and Bound or Dijkstra's algorithm)

  g(n) is the path cost to node n
 

 

Always expand least cost node first placing in queue ordered by path cost g(n). From our map, the step cost is the distance between towns. The fringe from initial state Arad is:

fringe = [(Zerind,75),(Timisoara,118),(Sibiu,140)]

After expanding Zerind:

fringe = [(Timisoara,118),(Sibiu,140), (Oradea,146)]


What are the next two fringe values?


Depth-limited search

  1. Show the search tree as states are explored for the state space assuming:

    INITIAL-STATE = { 'Find D' : 'A' }

    DEPTH-LIMITED-SEARCH('Find D', 3)

     

  2. Assuming:

    INITIAL-STATE = { 'Find D' : 'A' }

    DEPTH-LIMITED-SEARCH('Find H', 3)

 

Iterative deepening depth-first search

Applies DEPTH-LIMITED-SEARCH repeatedly, generating upper-level nodes multiple times.

Seems wasteful but most nodes in lower-levels of search tree so not too expensive.

Example

Goal M

 

 

 

Properties of iterative deepening

 

3.5 AVOIDING REPEATED STATES

Graph search

DFS loops if the search repeats a state.

Closed list holds all expanded states.

Graph search performs DFS, avoiding loops by expanding only states not on the closed list.

 

 

  1. What is the state space for the diagram at right?
  2. Using GRAPH-SEARCH, show the fringe as states are explored for the state space assuming:
     

    INITIAL-STATE = { 'Find C' : 'A' }

    GRAPH-SEARCH('Find C', [])

 

Summary of tree-search algorithms

* BFS complete if finite branching

* BFS optimal if step cost same to all states, then shallowest goal is least cost.

* Uniform-cost complete if step cost > 0 which ensures that cost increases along path to goal.

* Uniform-cost optimal for same reason complete.

 

  1. Why isn't DFS complete?
  2. Why isn't DFS optimal?