Strategy for a 8-Puzzle Solver.
get_successors(...)
method to get potential future steps.State Space
All possible combinations of tile positions (both numbered and blank tiles)
Each value of
Not a compact representation → Duplicates and unreachable states can be represented
As a result of this, we want to avoid enumerating states / constructing an explicit state graph
Action Space
World Dynamics
Utility Function
$0$ otherwise
The tile in every position matches between the current state and the goal
__init__
)class EightPuzzle:
def __init__(self, squares):
if type(squares) is str:
self.squares == list(squares)
else:
# Convert the list to string
self.squares = [str(i) for i in squares]
# Find position of _
idx = -1
for i in range(len(self.squares)):
if self.squares[i] == '_':
idx = i;
self.idx = idx
__eq__
)EightPuzzle
class), test if it is the same as this object.in
keyword.visited
collection)__eq__
function more rigorous, we could test for type.def __eq__(self, obj):;
if obj is None:
return False
# optional test for type:
"""
if type(obj) != EightPuzzle:
return False
"""
return tuple(self.squares) ==
tuple(obj.squares)
__hash__
)__hash__
method implemented allows a class to be inserted into Hash Table data structures, such as sets and dictionariesdef __hash__(self):
return hash(tuple(self.squares))
__hash__
function
__hash__
for data types that are immutable.get_successors(...)
Using a consistent format allows environments, algorithms to be reused - use consistent method names and output formats.
Return a collection of possible next states.
Must be able to recover the action associated with each next state
Helper methods for different movement directions (swap tiles in each direction)
Only return the next state for actions which are valid.
Valid actions depend on row and column (moving the blank tile up is not possible when the blank tile is in the top row).
Use the modulo (%) and integer division (//) operators to extract the row and column.
Here, set next_state=None
if the action is invalid.
Representing the successors in a dictionary may be more accessible.
def get_successors(self):
successors = []
if self.idx % 3 > 0:
successors.append(self.move_left())
else:
successors.append(None)
if self.idx % 3 < 2:
successors.append(self.move_right())
else:
successors.append(None)
if self.idx // 3 > 0:
successors.append(self.move_up())
else:
successors.append(None)
if self.idx // 3 < 2:
successors.append(self.move_down())
else:
successors.append(None)
return successors
Note that in this example, copy.deepcopy() is slower than the deep copy function in the constructor
new_squares = [str(i) for i in self.squares]
Use some other rules (functions) to prevent moving to an invalid state - in this case, we use the get_successors()
function to do this
def move_left(self):
new_squares = copy.deepcopy(self.squares)
new_squares[self.idx] = self.squares[self.idx-1]
new_squares[self.idx-1] = self.squares[self.idx]
return EightPuzzle(new_squares)
def move_right(self):
new_squares = copy.deepcopy(self.squares)
new_squares[self.idx] = self.squares[self.idx+1]
new_squares[self.idx+1] = self.squares[self.idx]
return EightPuzzle(new_squares)
def move_up(self):
new_squares = copy.deepcopy(self.squares)
new_squares[self.idx] = self.squares[self.idx-3]
new_squares[self.idx-3] = self.squares[self.idx]
return EightPuzzle(new_squares)
def move_down(self):
new_squares = copy.deepcopy(self.squares)
new_squares[self.idx] = self.squares[self.idx+3]
new_squares[self.idx+3] = self.squares[self.idx]
return EightPuzzle(new_squares)
get_successors
pattern:
class ContainerEntry:
def __init__(self, puzzle, actions):
self.puzzle = puzzle
self.actions = actions
def get_successors(self):
s = []
suc = self.puzzle.get_successors()
if suc[0] is not None:
s.append(ContainerEntry(suc[0],
self.actions + [LEFT]))
if suc[1] is not None:
s.append(ContainerEntry(suc[1],
self.actions + [RIGHT]))
if suc[2] is not None:
s.append(ContainerEntry(suc[2],
self.actions + [UP]))
if suc[3] is not None:
s.append(ContainerEntry(suc[3],
self.actions + [DOWN]))
return s
def __eq__(self, obj):
return self.puzzle == obj.puzzle
get_successors()
→ collection(SearchNode)
State.ACTIONS, nextState = State.perform_action(action
Return Current_Node.actions
If s not visited (or s visited at higher cost than current cost)
Add SearchNode(s) to the Container
Add s to the visited set.
# BFS Algorithm
def bfs(initial, goal):
container = [ContainerEntry(initial, [])]
visited = set([])
i = 0
while len(container) > 0:
# expand node
node = container.pop(0)
if node.puzzle == goal:
return node.actions
# add successors
suc = node.get_successors()
for s in suc:
if s.puzzle not in visited:
container.append(s)
visited.add(s.puzzle)
i += 1
return None
# DFS Algorithm
def dfs(initial, goal):
container = [ContainerEntry(initial, [])]
visited = set([])
i = 0
while len(container) > 0:
# expand node
node = container.pop(-1)
if node.puzzle == goal:
return node.actions
# add successors
suc = node.get_successors()
for s in suc:
if s.puzzle not in visited:
container.append(s)
visited.add(s.puzzle)
i += 1
return None
281_43765
to 1238_4765
281463_75
to 1238_4765
parity(initial) == parity(goal)
_12345678
def num_inversions(self):
total = 0
for i in range(len(self.squares)):
if self.squares[i] == '_':
continue
si = int(self.squares[i])
for j in range(i, len(self.squares)):
if self.squares[j] == '_':
continue
sj = int(self.squares[j])
if si > sj:
total += 1
return total
def get_parity(self):
return self.num_inversions() % 2
# In the main solution code, we write
if p1.get_parity() != p2.get_parity():
print('No solution')
return
cost = {'up':1, 'down':2, 'left':3, 'right':4}
cost == 1
), then UCS will expand nodes in the same order as BFS (producing the same solution)