Python: N Puzzle (8-Puzzle) Solver Heruistics: How to iterate? - python

I have written a selection of functions to try and solve a N-puzzle / 8-puzzle.
I am quite content with my ability to manipulate the puzzle but am struggling with how to iterate and find the best path. My skills are not in OOP either and so the functions are simple.
The idea is obviously to reduce the heruistic distance and place all pieces in their desired locations.
I have read up a lot of other questions regarding this topic but they're often more advanced and OOP focused.
When I try and iterate through there are no good moves. I'm not sure how to perform the A* algorithm.
from math import sqrt, fabs
import copy as cp
# Trial puzzle
puzzle1 = [
[3,5,4],
[2,1,0],
[6,7,8]]
# This function is used minimise typing later
def starpiece(piece):
'''Checks the input of a *arg and returns either tuple'''
if piece == ():
return 0
elif isinstance(piece[0], (str, int)) == True:
return piece[0]
elif isinstance(piece[0], (tuple, list)) and len(piece[0]) == 2:
return piece[0]
# This function creates the goal puzzle layout
def goal(puzzle):
'''Input a nested list and output an goal list'''
n = len(puzzle) * len(puzzle)
goal = [x for x in range(1,n)]
goal.append(0)
nested_goal = [goal[i:i+len(puzzle)] for i in range(0, len(goal), len(puzzle))]
return nested_goal
# This fuction gives either the coordinates (as a tuple) of a piece in the puzzle
# or the piece in the puzzle at give coordinates
def search(puzzle, *piece):
'''Input a puzzle and piece value and output a tuple of coordinates.
If no piece is selected 0 is chosen by default. If coordinates are
entered the piece value at those coordinates are outputed'''
piece = starpiece(piece)
if isinstance(piece, (tuple, list)) == True:
return puzzle[piece[0]][piece[1]]
for slice1, sublist in enumerate(puzzle):
for slice2, item in enumerate(sublist):
if puzzle[slice1][slice2] == piece:
x, y = slice1, slice2
return (x, y)
# This function gives the neighbours of a piece at a given position as a list of coordinates
def neighbours(puzzle, *piece):
'''Input a position (as a tuple) or piece and output a list
of adjacent neighbours. Default are the neighbours to 0'''
length = len(puzzle) - 1
return_list = []
piece = starpiece(piece)
if isinstance(piece, tuple) != True:
piece = search(puzzle, piece)
if (piece[0] - 1) >= 0:
x_minus = (piece[0] - 1)
return_list.append((x_minus, piece[1]))
if (piece[0] + 1) <= length:
x_plus = (piece[0] + 1)
return_list.append((x_plus, piece[1]))
if (piece[1] - 1) >= 0:
y_minus = (piece[1] - 1)
return_list.append((piece[0], y_minus))
if (piece[1] + 1) <= length:
y_plus = (piece[1] + 1)
return_list.append((piece[0], y_plus))
return return_list
# This function swaps piece values of adjacent cells
def swap(puzzle, cell1, *cell2):
'''Moves two cells, if adjacent a swap occurs. Default value for cell2 is 0.
Input either a cell value or cell cooridinates'''
cell2 = starpiece(cell2)
if isinstance(cell1, (str, int)) == True:
cell1 = search(puzzle, cell1)
if isinstance(cell2, (str, int)) == True:
cell2 = search(puzzle, cell2)
puzzleSwap = cp.deepcopy(puzzle)
if cell1 == cell2:
print('Warning: no swap occured as both cell values were {}'.format(search(puzzle,cell1)))
return puzzleSwap
elif cell1 in neighbours(puzzleSwap, cell2):
puzzleSwap[cell1[0]][cell1[1]], puzzleSwap[cell2[0]][cell2[1]] = puzzleSwap[cell2[0]][cell2[1]], puzzleSwap[cell1[0]][cell1[1]]
return puzzleSwap
else:
print('''Warning: no swap occured as cells aren't adjacent''')
return puzzleSwap
# This function gives true if a piece is in it's correct position
def inplace(puzzle, p):
'''Ouputs bool on whether a piece is in it's correct position'''
if search(puzzle, p) == search(goal(puzzle), p):
return True
else:
return False
# These functions give heruistic measurements
def heruistic(puzzle):
'''All returns heruistic (misplaced, total distance) as a tuple. Other
choices are: heruistic misplaced, heruistic distance or heruistic list'''
heruistic_misplaced = 0
heruistic_distance = 0
heruistic_distance_total = 0
heruistic_list = []
for sublist in puzzle:
for item in sublist:
if inplace(puzzle, item) == False:
heruistic_misplaced += 1
for sublist in puzzle:
for item in sublist:
a = search(puzzle, item)
b = search(goal(puzzle), item)
heruistic_distance = int(fabs(a[0] - b[0]) + fabs(a[1] - b[1]))
heruistic_distance_total += heruistic_distance
heruistic_list.append(heruistic_distance)
return (heruistic_misplaced, heruistic_distance_total, heruistic_list)
def hm(puzzle):
'''Outputs heruistic misplaced'''
return heruistic(puzzle)[0]
def hd(puzzle):
'''Outputs total heruistic distance'''
return heruistic(puzzle)[1]
def hl(puzzle):
'''Outputs heruistic list'''
return heruistic(puzzle)[2]
def hp(puzzle, p):
'''Outputs heruistic distance at a given location'''
x, y = search(puzzle, p)[0], search(puzzle, p)[1]
return heruistic(puzzle)[2][(x * len(puzzle)) + y]
# This is supposted to iterate along a route according to heruistics but doesn't work
def iterMove(puzzle):
state = cp.deepcopy(puzzle)
while state != goal(puzzle):
state_hd = hd(state)
state_hm = hm(state)
moves = neighbours(state)
ok_moves = []
good_moves = []
for move in moves:
maybe_state = swap(state, move)
if hd(maybe_state) < state_hd and hm(maybe_state) < state_hm:
good_moves.append(move)
elif hd(maybe_state) < state_hd:
ok_moves.append(move)
elif hm(maybe_state) < state_hm:
ok_moves.append(move)
if good_moves != []:
print(state)
state = swap(state, good_moves[0])
elif ok_moves != []:
print(state)
state = swap(state, ok_moves[0])
>> iterMove(puzzle1)
'no good moves'

To implement A* in Python you can use https://docs.python.org/3/library/heapq.html for a priority queue. You put possible positions into the queue with a priority of "cost so far + heuristic for remaining cost". When you take them out of the queue you check a set of already seen positions. Skip this one if you've seen the position, else add it to the set and then process.
An untested version of the critical piece of code:
queue = [(heuristic(starting_position), 0, starting_position, None)]
while 0 < len(queue):
(est_moves, cur_moves, position, history) = heapq.heappop(queue)
if position in seen:
continue
elif position = solved:
return history
else:
seen.add(position)
for move in possible_moves(position):
next_position = position_after_move(position, move)
est_moves = cur_moves + 1 + heuristic(next_position)
heapq.heappush(queue,
(est_moves, cur_moves+1,
next_position, (move, history)))
return None

Related

How can I implement IDA* algorithm in Python for 15-Puzzle problem?

I'm trying to solve the 15-Puzzle problem using IDA* algorithm and Manhattan heuristic.
I already implemented the algorithm from the pseudocode in this Wikipedia page (link).
Here's my code so far :
def IDA(initial_state, goal_state):
initial_node = Node(initial_state)
goal_node = Node(goal_state)
threshold = manhattan_heuristic(initial_state, goal_state)
path = [initial_node]
while 1:
tmp = search(path, goal_state, 0, threshold)
if tmp == True:
return path, threshold
elif tmp == float('inf'):
return False
else:
threshold = tmp
def search(path, goal_state, g, threshold):
node = path[-1]
f = g + manhattan_heuristic(node.state, goal_state)
if f > threshold:
return f
if np.array_equal(node.state, goal_state):
return True
minimum = float('inf')
for n in node.nextnodes():
if n not in path:
path.append(n)
tmp = search(path, goal_state, g + 1, threshold)
if tmp == True:
return True
if tmp < minimum:
minimum = tmp
path.pop()
return minimum
def manhattan_heuristic(state1, state2):
size = range(1, len(state1) ** 2)
distances = [count_distance(num, state1, state2) for num in size]
return sum(distances)
def count_distance(number, state1, state2):
position1 = np.where(state1 == number)
position2 = np.where(state2 == number)
return manhattan_distance(position1, position2)
def manhattan_distance(a, b):
return abs(b[0] - a[0]) + abs(b[1] - a[1])
class Node():
def __init__(self, state):
self.state = state
def nextnodes(self):
zero = np.where(self.state == 0)
y,x = zero
y = int(y)
x = int(x)
up = (y - 1, x)
down = (y + 1, x)
right = (y, x + 1)
left = (y, x - 1)
arr = []
for direction in (up, down, right, left):
if len(self.state) - 1 >= direction[0] >= 0 and len(self.state) - 1 >= direction[1] >= 0:
tmp = np.copy(self.state)
tmp[direction[0], direction[1]], tmp[zero] = tmp[zero], tmp[direction[0], direction[1]]
arr.append(Node(tmp))
return arr
I'm testing this code with a 3x3 Puzzle and here's the infinite loop! Due to the recursion I have some trouble testing my code...
I think the error might be here : tmp = search(path, goal_state, g + 1, threshold) (in the search function). I'm adding only one to the g cost value. It should be correct though, because I can only move a tile 1 place away.
Here's how to call the IDA() function:
initial_state = np.array([8 7 3],[4 1 2],[0 5 6])
goal_state = np.array([1 2 3],[8 0 4],[7 6 5])
IDA(initial_state, goal_state)
Can someone help me on this ?
There are couple of issues in your IDA* implementation. First, what is the purpose of the variable path? I found two purposes of path in your code:
Use as a flag/map to keep the board-states that is already been visited.
Use as a stack to manage recursion states.
But, it is not possible to do both of them by using a single data structure. So, the first modification that your code requires:
Fix-1: Pass current node as a parameter to the search method.
Fix-2: flag should be a data structure that can perform a not in query efficiently.
Now, fix-1 is easy as we can just pass the current visiting node as the parameter in the search method. For fix-2, we need to change the type of flag from list to set as:
list's average case complexity for x in s is: O(n)
set's
Average case complexity for x in s is: O(1)
Worst case complexity for x in s is: O(n)
You can check more details about performance for testing memberships: list vs sets for more details.
Now, to keep the Node information into a set, you need to implement __eq__ and __hash__ in your Node class. In the following, I have attached the modified code.
import timeit
import numpy as np
def IDA(initial_state, goal_state):
initial_node = Node(initial_state)
goal_node = Node(goal_state)
threshold = manhattan_heuristic(initial_state, goal_state)
#print("heuristic threshold: {}".format(threshold))
loop_counter = 0
while 1:
path = set([initial_node])
tmp = search(initial_node, goal_state, 0, threshold, path)
#print("tmp: {}".format(tmp))
if tmp == True:
return True, threshold
elif tmp == float('inf'):
return False, float('inf')
else:
threshold = tmp
def search(node, goal_state, g, threshold, path):
#print("node-state: {}".format(node.state))
f = g + manhattan_heuristic(node.state, goal_state)
if f > threshold:
return f
if np.array_equal(node.state, goal_state):
return True
minimum = float('inf')
for n in node.nextnodes():
if n not in path:
path.add(n)
tmp = search(n, goal_state, g + 1, threshold, path)
if tmp == True:
return True
if tmp < minimum:
minimum = tmp
return minimum
def manhattan_heuristic(state1, state2):
size = range(1, len(state1) ** 2)
distances = [count_distance(num, state1, state2) for num in size]
return sum(distances)
def count_distance(number, state1, state2):
position1 = np.where(state1 == number)
position2 = np.where(state2 == number)
return manhattan_distance(position1, position2)
def manhattan_distance(a, b):
return abs(b[0] - a[0]) + abs(b[1] - a[1])
class Node():
def __init__(self, state):
self.state = state
def __repr__(self):
return np.array_str(self.state.flatten())
def __hash__(self):
return hash(self.__repr__())
def __eq__(self, other):
return self.__hash__() == other.__hash__()
def nextnodes(self):
zero = np.where(self.state == 0)
y,x = zero
y = int(y)
x = int(x)
up = (y - 1, x)
down = (y + 1, x)
right = (y, x + 1)
left = (y, x - 1)
arr = []
for direction in (up, down, right, left):
if len(self.state) - 1 >= direction[0] >= 0 and len(self.state) - 1 >= direction[1] >= 0:
tmp = np.copy(self.state)
tmp[direction[0], direction[1]], tmp[zero] = tmp[zero], tmp[direction[0], direction[1]]
arr.append(Node(tmp))
return arr
initial_state = np.array([[8, 7, 3],[4, 1, 2],[0, 5, 6]])
goal_state = np.array([[1, 2, 3],[8, 0, 4],[7, 6, 5]])
start = timeit.default_timer()
is_found, th = IDA(initial_state, goal_state)
stop = timeit.default_timer()
print('Time: {} seconds'.format(stop - start))
if is_found is True:
print("Solution found with heuristic-upperbound: {}".format(th))
else:
print("Solution not found!")
Node: Please double check your Node.nextnodes() and manhattan_heuristic() methods as I did not pay much attention in those areas. You can check this GitHub repository for other algorithmic implementations (i.e., A*, IDS, DLS) to solve this problem.
References:
Python Wiki: Time Complexity
TechnoBeans: Performance for testing memberships: list vs tuples vs sets
GitHub: Puzzle Solver (by using problem solving techniques)

How does these two methods have different time complexity in Python?

I am trying to solve the Leetcode word search problem .
I have two solutions which to me is identical although one of them returns in Time Limit Exceeded.
I read all resources around this and could not understand what is happening behind the scene.
Solution 1 with optimised time complexity:
class Solution(object):
def bfs(self, position, visited, board, word):
# print(position)
if not word:
return True
destination = [(-1,0), (1,0), (0,1), (0,-1)]
res = False
for x,y in destination:
new_postion_x, new_postion_y = position[0] + x, position[1] + y
if (0<= new_postion_x < len(board)) and (0 <= new_postion_y < len(board[0])) and (new_postion_x, new_postion_y) not in visited:
if board[new_postion_x][new_postion_y] == word[0]:
new_visited = visited.copy()
new_visited[(new_postion_x,new_postion_y)] = 1
res = res or self.bfs((new_postion_x, new_postion_y), new_visited, board, word[1:])
return res
def exist(self, board, word):
for i in xrange(len(board)):
for j in xrange(len(board[0])):
if board[i][j] == word[0]:
t = self.bfs((i,j), {(i,j): 1}, board, word[1:])
if t:
return True
return False
Solution two which returns TLE on execution for large input:
class Solution(object):
def bfs(self, position, visited, board, word):
print(position)
if not word:
return True
destination = [(-1,0), (1,0), (0,1), (0,-1)]
res = False
for x,y in destination:
new_postion_x, new_postion_y = position[0] + x, position[1] + y
if (0<= new_postion_x < len(board)) and (0 <= new_postion_y < len(board[0])) and (new_postion_x, new_postion_y) not in visited:
if board[new_postion_x][new_postion_y] == word[0]:
new_visited = visited.copy()
new_visited[(new_postion_x,new_postion_y)] = 1
t = self.bfs((new_postion_x, new_postion_y), new_visited, board, word[1:])
res = res or t
return res
def exist(self, board, word):
for i in xrange(len(board)):
for j in xrange(len(board[0])):
if board[i][j] == word[0]:
t = self.bfs((i,j), {(i,j): 1}, board, word[1:])
if t:
return True
return False
The key line is here:
res = res or self.bfs((new_postion_x, new_postion_y), new_visited, board, word[1:])
Once res becomes True, this line will never execute self.bfs() again because the expression after the or does not need to be evaluated. This is called short-circuiting.
Thus, once a solution is found you don't make any more recursive calls and quickly return the True all the way back to the top.
An equivalent way of writing this, which might clarify it, would be:
if not res:
res = self.bfs(...)
In the other solution:
t = self.bfs((new_postion_x, new_postion_y), new_visited, board, word[1:])
res = res or t
This will always call self.bfs() even when it's not needed anymore, with the net effect of always searching through the entire graph even if a solution has already been found.

My A-star implementation seems very slow, need advice and help on what I am doing wrong

My tests of my implementations of Dijkstra and A-Star have revealed that my A-star implementation is approximately 2 times SLOWER. Usually equivalent implementations of Dijkstra and A-star should see A-star beating out Dijkstra. But that isn't the case here and so it has led me to question my implementation of A-star. So I want someone to tell me what I am doing wrong in my implementation of A-star.
Here is my code:
from copy import deepcopy
from math import inf, sqrt
import maze_builderV2 as mb
if __name__ == '__main__':
order = 10
space = ['X']+['_' for x in range(order)]+['X']
maze = [deepcopy(space) for x in range(order)]
maze.append(['X' for x in range(order+2)])
maze.insert(0, ['X' for x in range(order+2)])
finalpos = (order, order)
pos = (1, 1)
maze[pos[0]][pos[1]] = 'S' # Initializing a start position
maze[finalpos[0]][finalpos[1]] = 'O' # Initializing a end position
mb.mazebuilder(maze=maze)
def spit():
for x in maze:
print(x)
spit()
print()
mazemap = {}
def scan(): # Converts raw map/maze into a suitable datastructure.
for x in range(1, order+1):
for y in range(1, order+1):
mazemap[(x, y)] = {}
t = [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]
for z in t:
if maze[z[0]][z[1]] == 'X':
pass
else:
mazemap[(x, y)][z] = [sqrt((pos[0]-z[0])**2+(pos[1]-z[1])**2),
sqrt((finalpos[0]-z[0])**2+(finalpos[1]-z[1])**2)] # Euclidean distance to destination (Heuristic)
scan()
unvisited = deepcopy(mazemap)
distances = {}
paths = {}
# Initialization of distances:
for node in unvisited:
if node == pos:
distances[node] = [0, sqrt((finalpos[0]-node[0])**2+(finalpos[1]-node[1])**2)]
else:
distances[node] = [inf, inf]
while unvisited != {}:
curnode = None
for node in unvisited:
if curnode == None:
curnode = node
elif (distances[node][0]+distances[node][1]) < (distances[curnode][0]+distances[curnode][1]):
curnode = node
else:
pass
for childnode, lengths in mazemap[curnode].items():
# Length to nearby childnode - G length, Euclidean (Heuristic) length from curnode to finalpos - H length
# G length + H length < Euclidean length to reach that childnode directly + Euclidean length to finalpos from that childnode = Better path found, update known distance and paths
if lengths[0] + lengths[1] < distances[childnode][0] + distances[childnode][1]:
distances[childnode] = [lengths[0], lengths[1]]
paths[childnode] = curnode
unvisited.pop(curnode)
def shortestroute(paths, start, end):
shortestpath = []
try:
def rec(start, end):
if end == start:
shortestpath.append(end)
return shortestpath[::-1]
else:
shortestpath.append(end)
return rec(start, paths[end])
return rec(start, end)
except KeyError:
return False
finalpath = shortestroute(paths, pos, finalpos)
if finalpath:
for x in finalpath:
if x == pos or x == finalpos:
pass
else:
maze[x[0]][x[1]] = 'W'
else:
print("This maze not solvable, Blyat!")
print()
spit()
For those who find my code too messy and can't bother to read the comments I added to help with the reading... Here is a gist of my code:
Creates a mazemap (all the coordinates and its connected neighbors along with their euclidean distances from that neighboring point to the start position (G Cost) as well as to the final position (H Cost)... in a dictionary)
start position is selected as the current node. All distances to other nodes is initialised as infinity.
For every node we compare the total path cost i.e is the G cost + H cost. The one with least total cost is selected as then next current node. Each time we select new current node, we add that node to a dictionary that keeps track of through which node it was reached, so that it is easier to backtrack and find our path.
Process continues until current node is the final position.
If anyone can help me out on this, that would be great!
EDIT: On account of people asking for the maze building algorithm, here it is:
# Maze generator - v2: Generates mazes that look like city streets (more or less...)
from copy import deepcopy
from random import randint, choice
if __name__ == "__main__":
order = 10
space = ['X']+['_' for x in range(order)]+['X']
maze = [deepcopy(space) for x in range(order)]
maze.append(['X' for x in range(order+2)])
maze.insert(0, ['X' for x in range(order+2)])
pos = (1, 1)
finalpos = (order, order)
maze[pos[0]][pos[1]] = 'S' # Initializing a start position
maze[finalpos[1]][finalpos[1]] = 'O' # Initializing a end position
def spit():
for x in maze:
print(x)
blocks = []
freespaces = [(x, y) for x in range(1, order+1) for y in range(1, order+1)]
def blockbuilder(kind):
param1 = param2 = 0
double = randint(0, 1)
if kind == 0:
param2 = randint(3, 5)
if double:
param1 = 2
else:
param1 = 1
else:
param1 = randint(3, 5)
if double:
param2 = 2
else:
param2 = 1
for a in range(blockstarter[0], blockstarter[0]+param2):
for b in range(blockstarter[1], blockstarter[1]+param1):
if (a+1, b) in blocks or (a-1, b) in blocks or (a, b+1) in blocks or (a, b-1) in blocks or (a, b) in blocks or (a+1, b+1) in blocks or (a-1, b+1) in blocks or (a+1, b-1) in blocks or (a-1, b-1) in blocks:
pass
else:
if a > order+1 or b > order+1:
pass
else:
if maze[a][b] == 'X':
blocks.append((a, b))
else:
spaces = [(a+1, b), (a-1, b), (a, b+1), (a, b-1)]
for c in spaces:
if maze[c[0]][c[1]] == 'X':
break
else:
maze[a][b] = 'X'
blocks.append((a, b))
for x in range(1, order+1):
for y in range(1, order+1):
if (x, y) in freespaces:
t = [(x+1, y), (x-1, y), (x, y+1), (x, y-1)]
i = 0
while i < len(t):
if maze[t[i][0]][t[i][1]] == 'X' or (t[i][0], t[i][1]) == pos or (t[i][0], t[i][1]) == finalpos:
del t[i]
else:
i += 1
if len(t) > 2:
blockstarter = t[randint(0, len(t)-1)]
kind = randint(0, 1) # 0 - vertical, 1 - horizontal
blockbuilder(kind)
else:
pass
# rch = choice(['d', 'u', 'r', 'l'])
b = 0
while b < len(blocks):
block = blocks[b]
t = {'d': (block[0]+2, block[1]), 'u': (block[0]-2, block[1]),
'r': (block[0], block[1]+2), 'l': (block[0], block[1]-2)}
rch = choice(['d', 'u', 'r', 'l'])
z = t[rch]
# if z[0] > order+1 or z[1] > order+1 or z[0] < 1 or z[1] < 1:
# Decreased chance of having non solvable maze being generated...
if z[0] > order-2 or z[1] > order-2 or z[0] < 2+2 or z[1] < 2+2:
pass
else:
if maze[z[0]][z[1]] == 'X':
if randint(0, 1):
set = None
if rch == 'u':
set = (z[0]+1, z[1])
elif rch == 'd':
set = (z[0]-1, z[1])
elif rch == 'r':
set = (z[0], z[1]-1)
elif rch == 'l':
set = (z[0], z[1]+1)
else:
pass
if maze[set[0]][set[1]] == '_':
# Checks so that no walls that block the entire way are formed
# Makes sure maze is solvable
sets, count = [
(set[0]+1, set[1]), (set[0]-1, set[1]), (set[0], set[1]+1), (set[0], set[1]-1)], 0
for blyat in sets:
while blyat[0] != 0 and blyat[1] != 0 and blyat[0] != order+1 and blyat[1] != order+1:
ch = [(blyat[0]+1, blyat[1]), (blyat[0]-1, blyat[1]),
(blyat[0], blyat[1]+1), (blyat[0], blyat[1]-1)]
suka = []
for i in ch:
if ch not in suka:
if maze[i[0]][i[1]] == 'X':
blyat = i
break
else:
pass
suka.append(ch)
else:
pass
else:
blyat = None
if blyat == None:
break
else:
pass
else:
count += 1
if count < 1:
maze[set[0]][set[1]] = 'X'
blocks.append(set)
else:
pass
else:
pass
else:
pass
b += 1
mazebuilder(maze, order)
spit()
Sorry for leaving this out!
Just at a quick glance, it looks like you don't have a closed set at all?? Your unvisited structure appears to contain every node in the map. This algorithm is not A* at all.
Once you fix that, make sure to change unvisited from a list to a priority queue also.

optimizing Knight's tour on a chess board

my code below
I have a little knight's tour problem I'm trying to solve: find the smallest number of moves from point A to point B on an N*N chess board.
I created a board, and used a simple algorithm:
1. add point A to candidate list and start loop:
2. pop first element in candidate list and check it:
3. if end - return counter
4. else - add the candidate 's "sons" to end of candidate list
5. go to step 2 (counter is incremented after all previous level sons are popped)
This algorithm works as I expected (used it on a few test cases), but it was very slow:
The call f = Find_route(20, Tile(4,4), Tile(14,11)) (20 is the board dimensions, Tile(4,4) and Tile(14,11) are the start & end positions, respectively) checked 201590 (!!) tiles before reaching the answer.
I tried optimizing it by sorting the candidates list with sorted(tiles, key = lambda e : abs(e.x - end.x)+abs(e.y - end.y)) where tiles is the candidates list. That works for some of the cases but for some it is kind of useless.
helpful cases:
f = Find_route(20, Tile(1,4), Tile(1,10)) from 459 to 309 (~33% !!)
f = Find_route(20, Tile(7,0), Tile(1,11)) from 87738 to 79524 (~10% :( )
unhelpful cases:
f = Find_route(20, Tile(4,4), Tile(14,11)): from 201891 to 201590
f = Find_route(20, Tile(1,4), Tile(1,11)) from 2134 to 2111
I want eventually to have a list of near-end cases, from which the algorithm would know exactly what to do, (something like a 5 tiles radius), and I think that could help, but I am more interested in how to improve my optimize_list method. Any tips?
Code
class Tile(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
tmp = '({0},{1})'.format(self.x, self.y)
return tmp
def __eq__(self, new):
return self.x == new.x and self.y == new.y
def get_horse_jumps(self, max_x , max_y):
l = [(1,2), (1,-2), (-1,2), (-1,-2), (2,1), (2,-1), (-2,1), (-2,-1)]
return [Tile(self.x + i[0], self.y + i[1]) for i in l if (self.x + i[0]) >= 0 and (self.y + i[1]) >= 0 and (self.x + i[0]) < max_x and (self.y + i[1]) < max_y]
class Board(object):
def __init__(self, n):
self.dimension = n
self.mat = [Tile(x,y) for y in range(n) for x in range(n)]
def show_board(self):
print('-'*20, 'board', '-'*20)
n = self.dimension
s = ''
for i in range(n):
for j in range(n):
s += self.mat[i*n + j].__str__()
s += '\n'
print(s,end = '')
print('-'*20, 'board', '-'*20)
class Find_route(Board):
def __init__(self, n, start, end):
super(Find_route, self).__init__(n)
#self.show_board()
self.start = start
self.end = end
def optimize_list(self, tiles, end):
return sorted(tiles, key = lambda e : abs(e.x - end.x)+abs(e.y - end.y))
def find_shortest_path(self, optimize = False):
counter = 0
sons = [self.start]
next_lvl = []
num_of_checked = 0
while True:
curr = sons.pop(0)
num_of_checked += 1
if curr == self.end:
print('checked: ', num_of_checked)
return counter
else: # check sons
next_lvl += curr.get_horse_jumps(self.dimension, self.dimension)
# sons <- next_lvl (optimize?)
# next_lvl <- []
if sons == []:
counter += 1
if optimize:
sons = self.optimize_list(next_lvl, self.end)
else:
sons = next_lvl
next_lvl = []
optimize = True
f = Find_route(20, Tile(7,0), Tile(1,11))
print(f.find_shortest_path(optimize))
print(f.find_shortest_path())
EDIT
I added another optimization level - optimize list at any insertion of new candidate tiles, and it seems to work like a charm, for some cases:
if optimize == 2:
if sons == []:
#counter += 1
sons = self.optimize_list(next_lvl, self.end)
else:
sons = self.optimize_list(sons + next_lvl, self.end)
else:
if sons == []:
counter += 1
if optimize == 1:
sons = self.optimize_list(next_lvl, self.end)
else:
sons = next_lvl
next_lvl = []
optimize = 2
f = Find_route(20, Tile(1,4), Tile(8,18)) # from 103761 to 8 ( optimal!!! )
print(f.find_shortest_path(optimize))
print(f.find_shortest_path())
I have a problem with calculating the number-of-jumps because I don't know when to increment the counter (maybe at each check?), but it seems to at least converge faster. Also, for other cases (e.g. f = Find_route(20, Tile(1,4), Tile(8,17))) it does not improve at all (not sure if it stops...)
Don't reinvent the wheel.
Build a graph with tiles as vertices. Connect tiles with an edge if a knight can get from one tile to another in one step.
Use a standard path finding algorithm. The breadth-first search looks like the best option in you're looking for a shortest path in an unweighted graph.

Conway's Game of Life using Sparse Matrix (Python)

I am writing the game of life in python using a Sparse Matrix. My program takes coordinates from user input and sets the cells at the chosen coordinates to be alive. I have it to where it will print the first generation, but I can't seem to figure out why it wont print any subsequent generations. Any help would be appreciated.
class SparseLifeGrid:
generations = list()
def __init__(self):
"""
"pass" just allows this to run w/o crashing.
Replace it with your own code in each method.
"""
self._mat = list()
self.col = []
self.row = []
def minRange(self):
"""
Return the minimum row & column as a tuple.
"""
for i in range(len(self.row)):
if self.row[i] < self.minRow:
self.minRow = self.row[i]
if self.col[i] < self.minCol:
self.minCol = self.col[i]
min_Range = [self.minRow,self.minCol]
return min_Range
def maxRange(self):
"""
Returns the maximum row & column as a tuple.
"""
for i in range(len(self.row)):
if self.row[i] > self.maxRow:
self.maxRow = self.row[i]
if self.col[i] > self.maxCol:
self.maxCol = self.col[i]
max_Range = [self.maxRow,self.maxCol]
return max_Range
def configure(self,coordList):
"""
Set up the initial board position.
"coordlist" is a list of coordinates to make alive.
"""
# for i in coordList:
# self.setCell(i[0],i[1])
self._mat = list()
self.coordList = coordList
for i in range(len(self.coordList)):
spot = self.coordList[i]
self.row += [spot[0]]
self.col += [spot[1]]
self._mat += [[self.row[i],self.col[i]]]
self.maxRow = self.minRow = self.row[0]
self.maxCol = self.minCol = self.col[0]
def clearCell(self,row, col):
"""
Set the cell to "dead" (False)
"""
self[row,col] = 0
def setCell(self,row, col):
"""
Set the cell to "live" (True") and if necessary, expand the
minimum or maximum range.
"""
self[row,col] = 1
def isLiveCell(self,row,col):
n = len(self.coordList)
for i in range(n):
if (self._mat[i] == [row,col]):
return True
return False
def numLiveNeighbors(self, row,col):
"""
Returns the number of live neighbors a cell has.
"""
neighbors = 0
if self.isLiveCell(row+1,col): #checks below the current cell
neighbors += 1
if self.isLiveCell(row-1,col): #checks above the current cell
neighbors += 1
if self.isLiveCell(row,col+1): #checks to the right of the current cell
neighbors += 1
if self.isLiveCell(row,col-1): #checks to the left of the current cell
neighbors += 1
if self.isLiveCell(row+1,col+1): #checks downwards diagonally to the right of the current cell
neighbors += 1
if self.isLiveCell(row+1,col-1): #checks downwards diagonally to the left of the current cell
neighbors += 1
if self.isLiveCell(row-1,col+1): #checks upwards diagonally to the right of the current cell
neighbors += 1
if self.isLiveCell(row-1,col-1): #checks upawards diagonally to the left of the current cell
neighbors += 1
return neighbors
def __getitem__(self,ndxTuple):
row = ndxTuple[0]
col = ndxTuple[1]
if(self.isLiveCell(row,col)==1):
return 1
else:
return 0
def __setitem__(self,ndxTuple, life):
"""
The possible values are only true or false:
True says alive, False for dead.
Also, check to see if this cell is outside of the maximum row and/or
column. If it is, modify the maximum row and/or maximum column.
"""
ndx = self._findPosition(ndxTuple[0],ndxTuple[1])
if ndx != None:
if life != True:
self._mat[ndx].value = life
else:
self._mat.pop[ndx]
else:
if life != True:
element = _GoLMatrixElement(ndxTuple[0],ndxTuple[1],life)
self._mat.append(element)
def _findPosition(self,row,col):
''' Does a search through the matrix when given the row&col and
returns the index of the element if found
'''
n = len(self._mat)
for i in range(n):
if (row == self._mat[i]) and (col == self._mat[i]):
return i
return None
def __str__(self):
"""
Print a column before and after the live cells
"""
s=""
maxRange=self.maxRange()
minRange=self.minRange()
for i in range(minRange[0]-1,maxRange[0]+2):
for j in range(minRange[1]-1,maxRange[1]+2):
s+=" "+str(self[i,j])
s+="\n"
return s
def getCopy(self):
"""
Return a copy of the current board object, including the max and min
values, etc.
"""
return SparseLifeGrid()
def evolve(self):
"""
Save the current state to the "generations" list.
Based on the current generation, return the next generation state.
"""
self.generations.append(self._mat)
for row in range(len(self.row)):
for col in range(len(self.col)):
if ((self[row,col] == True) and (self.numLiveNeighbors(row,col) == 2)):
self.setCell(row,col)
if ((self[row,col] == True) and (self.numLiveNeighbors(row,col) == 3)):
self.setCell(row,col)
if ((self[row,col] == True) and (self.numLiveNeighbors(row,col) < 2)):
self.clearCell(row,col)
if ((self[row,col] == True) and (self.numLiveNeighbors(row,col) > 3)):
self.clearCell(row,col)
if ((self[row,col] == False) and (self.numLiveNeighbors(row,col) == 3)):
self.setCell(row,col)
self.generations.append(self._mat)
return self._mat
def hasOccurred(self):
"""
Check whether this current state has already occured.
If not, return False. If true, return which generation number (1-10).
"""
for i in range(len(self.generations)):
if len(self.generations) > 0:
print("This is generation",len(self.generations))
return self.generations[i]
else:
print("No Generations")
return False
def __eq__(self,other):
"""
This is good method if we want to compare two sparse matrices.
You can just use "sparseMatrixA == sparseMatrixB" once this method
is working.
"""
pass
class _GoLMatrixElement:
"""
Storage class for one cell
"""
def __init__(self,row,col):
self.row = row
self.col = col
self.next = None #
# Since this node exists, this cell is now alive!
# To kill it, we just delete this node from the lists.
from SparseLifeGrid import SparseLifeGrid
import sys
def readPoints(lifeGrid):
"""
Reads the locations of life and set to the SparseMatrix
"""
print("1. Enter positions of life with row,col format (e.g., 2,3).")
print("2. Enter empty line to stop.")
life=input()
coordList=[]
while life:
points=life.split(",")
try:
coord=[int(points[0]),int(points[1])]
coordList.append(coord)
except ValueError:
print("Ignored input:" + life+ ", row, col not valid numbers")
except:
print("Unexpected error:", sys.exc_info()[0])
print("added, keep entering or enter empty line to stop.")
life=input()
print("Thanks, finished entering live cells")
lifeGrid.configure(coordList)
def main():
"""
Runs for ten generations if a stable (repeating) state is not found.
"""
lifeGrid= SparseLifeGrid()
readPoints(lifeGrid)
patterns=0
i=0
while i <10 :
"""
Evolve to the next generation
"""
lifeGrid.evolve()
print(lifeGrid)
"""
Check whether this generation is a repetition of any of the
previous states.
If yes return the previous matching generation (1-10).
"""
patterns=lifeGrid.hasOccurred()
if patterns != -1:
break
i+=1
if i==10:
print("No pattern found")
else:
print("Pattern found at: " + str(i)+ " of type: " + str(patterns))
main()
The loop only executes once because of this:
patterns=lifeGrid.hasOccurred()
if patterns != -1:
break
patterns will never equal -1 under any circumstance, since hasOccurred can only return False or a member of self.generations or None. As a result, the loop will always break in the first iteration, and no subsequent generations will be printed.
Incidentally, your hasOccurred logic is strange. if len(self.generations) equals zero, then the loop will not get executed at all, and none of your return statements will be evaluated, so the result will be None. If the length is greater than zero, then the condition within the loop is always True, so self.generations[i] is always returned in the first iteration. Perhaps you meant to do:
#changed the name from `hasOcurrence`, since that implies the method can only return True or False.
def get_last_occurrence(self):
for i in range(len(self.generations)):
#todo: somehow compare self.generations[i] to the current generation
if the generations match:
return i
#the loop ended without finding a match!
return False
Then, within main:
patterns=lifeGrid.hasOccurred()
if patterns != False:
break

Categories

Resources