Chapter 3
|
- Goal such as: In Bucharest
- State is some description of current world; In Arad
- Can determine available paths (sequence of actions) leading to goal to decide best.
- Search is process of looking for sequence of actions to find goal.
- Search algorithm returns solution as a sequence of actions.
- Problem is a problem formulation, the process of deciding actions and states to consider.
Well-defined problems and solutions
Problem has 4 components:
- Initial state of agent
In(Arad)
- Possible actions often implemented as a successor function.
successor( In(Arad) ) returns
{ <Go(Sibiu), In(Sibiu)>,
<Go(Timisoara), In(Timisoara)>,
<Go(Zerind), In(Zerind)>
}- Goal test determines if a state is the goal.
In (Bucharest)
- Path cost assigns numeric cost to each path to allow determining best. Possible metrics are: distance, time, etc.
Vacuum world
- States - 2 locations, agent in or not in a location, Clean or Dirty: 8 states total
- Initial state - any state can be initial state
- Successor function - Generates all legal states from actions (Left, Right, Suck)
- Goal test - check all squares are clean.
- Path cost - Each step costs 1 so path cost is number of steps in path.
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) : ...
- Initial state - any state can be initial state: (A, Dirty, Dirty)
- Successor function - Generates legal states from actions (Left, Right, Suck): (A, Clean, Dirty)
- Goal test - match a state where all squares are clean:
(A, Clean, Clean) or (B, Clean, Clean)
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.
States - Any city on map
Initial state - In Arad
Successor function - Generate all connected cities alphabetically.
Goal test - In Bucharest.




the node references a parent
node and a state.
Measuring problem-solving performance

Breadth-first search

![]() fringe = [ A ]
|
![]() fringe = [ B, C ] |
![]() fringe = [ C, D, E ] |
![]()
|
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 = [ 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 successorsExample
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']
- 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)?
- For goal J, give the fringe (successor list) after expanding each node.
- 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?
- 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).
- Incremental formulation where successor function calculates rather than looks up successor states:
- States: Arrangement of 0 to N queens, one per column in non-conflicting rows.
- Initial state: No queens on board. 4-queens is [None, None, None, None]
- Successor function: List of queens in leftmost empty column in non-conflicting rows.
- Goal test: N queens on board in non-conflicting positions.
- State passed to successor_fn function consists of list:
[col 0, ..., col N-1]
where col i is None if no queen is placed in the column or the row number of
the queen.
- The diagram at right illustrates the state represented by:
[1, None, None, None]
for which the successor_fn returns a list of all rows in column 1 (leftmost column with None) that are non-conflicting:
[ [1, 3, None, None] ]
What is the list returned by successor_fn for the state: [1,3,None,None]? - Nodes of first solution path found (in reverse order) for 4-queens is:
- [2, 0, 3, 1]
- [2, 0, 3, None]
- [2, 0, None, None]
- [2, None, None, None]
- [None, None, None, None]
- Note that the successor_fn produced for state:
[None, None, None, None]
the list of non-conflicting states:
[ [0, None, None, None], [1, None, None, None],
[2, None, None, None], [3, None, None, None]]The state explored next from the fringe is:
[0, None, None, None]
for which the list of non-conflicting states is:
[ [0, 2, None, None], [0, 3, None, None] ]
and the fringe now is:
[[0, 2, None, None], [0, 3, None, None],
[1, None, None, None], [2, None, None, None], [3, None, None, None]]
For the figure at right the queen in column 2, row 3 is in conflict.
- Given state [0, 2, None, None] what should the sucessor_fn return?
- Given the following fringe, what should be the new fringe after calling the successor_fn?
[ [0, 3, None, None],
[1, None, None, None], [2, None, None, None], [3, None, None, None]]
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.
- What is the result of successor_fn( [1, None, None, None])?
- What is the number of possible states for placing one queen on an empty 4x4 board?
- What is the number of possible states on a 4x4 board after placing 1 queen?
- What are the total number of possible states for the 4-queens problem?
- Is this a depth or breadth-first search?
- 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
Equivalent to depth-first search with limit L (i.e. nodes at depth L have no successors)
- Show the search tree as states are explored for the state space
assuming:
INITIAL-STATE = { 'Find D' : 'A' }
DEPTH-LIMITED-SEARCH('Find D', 3)
- 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

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.

- What is the state space for the
diagram at right?
- Using GRAPH-SEARCH, show the fringe as states are explored for the state space assuming:
INITIAL-STATE = { 'Find C' : 'A' }
GRAPH-SEARCH('Find C', [])

* 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.
- Why isn't DFS complete?
- Why isn't DFS optimal?