-
-
Save flatline/838202 to your computer and use it in GitHub Desktop.
| # Solves a randomized 8-puzzle using A* algorithm with plug-in heuristics | |
| import random | |
| import math | |
| _goal_state = [[1,2,3], | |
| [4,5,6], | |
| [7,8,0]] | |
| def index(item, seq): | |
| """Helper function that returns -1 for non-found index value of a seq""" | |
| if item in seq: | |
| return seq.index(item) | |
| else: | |
| return -1 | |
| class EightPuzzle: | |
| def __init__(self): | |
| # heuristic value | |
| self._hval = 0 | |
| # search depth of current instance | |
| self._depth = 0 | |
| # parent node in search path | |
| self._parent = None | |
| self.adj_matrix = [] | |
| for i in range(3): | |
| self.adj_matrix.append(_goal_state[i][:]) | |
| def __eq__(self, other): | |
| if self.__class__ != other.__class__: | |
| return False | |
| else: | |
| return self.adj_matrix == other.adj_matrix | |
| def __str__(self): | |
| res = '' | |
| for row in range(3): | |
| res += ' '.join(map(str, self.adj_matrix[row])) | |
| res += '\r\n' | |
| return res | |
| def _clone(self): | |
| p = EightPuzzle() | |
| for i in range(3): | |
| p.adj_matrix[i] = self.adj_matrix[i][:] | |
| return p | |
| def _get_legal_moves(self): | |
| """Returns list of tuples with which the free space may | |
| be swapped""" | |
| # get row and column of the empty piece | |
| row, col = self.find(0) | |
| free = [] | |
| # find which pieces can move there | |
| if row > 0: | |
| free.append((row - 1, col)) | |
| if col > 0: | |
| free.append((row, col - 1)) | |
| if row < 2: | |
| free.append((row + 1, col)) | |
| if col < 2: | |
| free.append((row, col + 1)) | |
| return free | |
| def _generate_moves(self): | |
| free = self._get_legal_moves() | |
| zero = self.find(0) | |
| def swap_and_clone(a, b): | |
| p = self._clone() | |
| p.swap(a,b) | |
| p._depth = self._depth + 1 | |
| p._parent = self | |
| return p | |
| return map(lambda pair: swap_and_clone(zero, pair), free) | |
| def _generate_solution_path(self, path): | |
| if self._parent == None: | |
| return path | |
| else: | |
| path.append(self) | |
| return self._parent._generate_solution_path(path) | |
| def solve(self, h): | |
| """Performs A* search for goal state. | |
| h(puzzle) - heuristic function, returns an integer | |
| """ | |
| def is_solved(puzzle): | |
| return puzzle.adj_matrix == _goal_state | |
| openl = [self] | |
| closedl = [] | |
| move_count = 0 | |
| while len(openl) > 0: | |
| x = openl.pop(0) | |
| move_count += 1 | |
| if (is_solved(x)): | |
| if len(closedl) > 0: | |
| return x._generate_solution_path([]), move_count | |
| else: | |
| return [x] | |
| succ = x._generate_moves() | |
| idx_open = idx_closed = -1 | |
| for move in succ: | |
| # have we already seen this node? | |
| idx_open = index(move, openl) | |
| idx_closed = index(move, closedl) | |
| hval = h(move) | |
| fval = hval + move._depth | |
| if idx_closed == -1 and idx_open == -1: | |
| move._hval = hval | |
| openl.append(move) | |
| elif idx_open > -1: | |
| copy = openl[idx_open] | |
| if fval < copy._hval + copy._depth: | |
| # copy move's values over existing | |
| copy._hval = hval | |
| copy._parent = move._parent | |
| copy._depth = move._depth | |
| elif idx_closed > -1: | |
| copy = closedl[idx_closed] | |
| if fval < copy._hval + copy._depth: | |
| move._hval = hval | |
| closedl.remove(copy) | |
| openl.append(move) | |
| closedl.append(x) | |
| openl = sorted(openl, key=lambda p: p._hval + p._depth) | |
| # if finished state not found, return failure | |
| return [], 0 | |
| def shuffle(self, step_count): | |
| for i in range(step_count): | |
| row, col = self.find(0) | |
| free = self._get_legal_moves() | |
| target = random.choice(free) | |
| self.swap((row, col), target) | |
| row, col = target | |
| def find(self, value): | |
| """returns the row, col coordinates of the specified value | |
| in the graph""" | |
| if value < 0 or value > 8: | |
| raise Exception("value out of range") | |
| for row in range(3): | |
| for col in range(3): | |
| if self.adj_matrix[row][col] == value: | |
| return row, col | |
| def peek(self, row, col): | |
| """returns the value at the specified row and column""" | |
| return self.adj_matrix[row][col] | |
| def poke(self, row, col, value): | |
| """sets the value at the specified row and column""" | |
| self.adj_matrix[row][col] = value | |
| def swap(self, pos_a, pos_b): | |
| """swaps values at the specified coordinates""" | |
| temp = self.peek(*pos_a) | |
| self.poke(pos_a[0], pos_a[1], self.peek(*pos_b)) | |
| self.poke(pos_b[0], pos_b[1], temp) | |
| def heur(puzzle, item_total_calc, total_calc): | |
| """ | |
| Heuristic template that provides the current and target position for each number and the | |
| total function. | |
| Parameters: | |
| puzzle - the puzzle | |
| item_total_calc - takes 4 parameters: current row, target row, current col, target col. | |
| Returns int. | |
| total_calc - takes 1 parameter, the sum of item_total_calc over all entries, and returns int. | |
| This is the value of the heuristic function | |
| """ | |
| t = 0 | |
| for row in range(3): | |
| for col in range(3): | |
| val = puzzle.peek(row, col) - 1 | |
| target_col = val % 3 | |
| target_row = val / 3 | |
| # account for 0 as blank | |
| if target_row < 0: | |
| target_row = 2 | |
| t += item_total_calc(row, target_row, col, target_col) | |
| return total_calc(t) | |
| #some heuristic functions, the best being the standard manhattan distance in this case, as it comes | |
| #closest to maximizing the estimated distance while still being admissible. | |
| def h_manhattan(puzzle): | |
| return heur(puzzle, | |
| lambda r, tr, c, tc: abs(tr - r) + abs(tc - c), | |
| lambda t : t) | |
| def h_manhattan_lsq(puzzle): | |
| return heur(puzzle, | |
| lambda r, tr, c, tc: (abs(tr - r) + abs(tc - c))**2, | |
| lambda t: math.sqrt(t)) | |
| def h_linear(puzzle): | |
| return heur(puzzle, | |
| lambda r, tr, c, tc: math.sqrt(math.sqrt((tr - r)**2 + (tc - c)**2)), | |
| lambda t: t) | |
| def h_linear_lsq(puzzle): | |
| return heur(puzzle, | |
| lambda r, tr, c, tc: (tr - r)**2 + (tc - c)**2, | |
| lambda t: math.sqrt(t)) | |
| def h_default(puzzle): | |
| return 0 | |
| def main(): | |
| p = EightPuzzle() | |
| p.shuffle(20) | |
| print p | |
| path, count = p.solve(h_manhattan) | |
| path.reverse() | |
| for i in path: | |
| print i | |
| print "Solved with Manhattan distance exploring", count, "states" | |
| path, count = p.solve(h_manhattan_lsq) | |
| print "Solved with Manhattan least squares exploring", count, "states" | |
| path, count = p.solve(h_linear) | |
| print "Solved with linear distance exploring", count, "states" | |
| path, count = p.solve(h_linear_lsq) | |
| print "Solved with linear least squares exploring", count, "states" | |
| # path, count = p.solve(heur_default) | |
| # print "Solved with BFS-equivalent in", count, "moves" | |
| if __name__ == "__main__": | |
| main() |
About the initial state, you can def the set function and set it in the main function.
p.set("123456078")
def set(self, other) :
i=0;
for row in range(3):
for col in range(3):
self.adj_matrix[row][col] = int(other[i])
i=i+1
can you explain this properly/?
Can anybody explain why this won't work with a goal state [[0,1,2], [3,4,5], [6,7,8]]? Many thanks.
@elstoneo It worked just fine for me.
will somebody tell me that are there different heuristics used with manhattan or totally different then manhattan
how can i print fval of optimal solution path
The call at line 231 path, count = p.solve(h_manhattan) issues the following error ... on an irregular basis!!
"ValueError: need more than 1 value to unpack" (PY 2)
"ValueError: not enough values to unpack (expected 2, got 1)" (PY 3)
Amazing that no one mentions it! This obnoxious error happens because the 'solve()' function returns a null path when the finished stated is not reached, whereas it should exit with a message or use some means for the caller to detect the failure, e.g. 'count' = -1 … There are dozens of ways to do it!
How could I define the initial state?
Thank you for your answer.