How do I find shortest path in maze with BFS? - python

I am trying to find a way to solve a maze. My teacher said I have to use BFS as a way to learn. So I made the algorithm itself, but I don't understand how to get the shortest path out of it. I have looked at others their codes and they said that backtracking is the way to do it. How does this backtracking work and what do you backtrack?
I will give my code just because I like some feedback to it and maybe I made some mistake:
def main(self, r, c):
running = True
self.queue.append((r, c))
while running:
if len(self.queue) > 0:
self.current = self.queue[0]
if self.maze[self.current[0] - 1][self.current[1]] == ' ' and not (self.current[0] - 1, self.current[1])\
in self.visited and not (self.current[0] - 1, self.current[1]) in self.queue:
self.queue.append((self.current[0] - 1, self.current[1]))
elif self.maze[self.current[0] - 1][self.current[1]] == 'G':
return self.path
if self.maze[self.current[0]][self.current[1] + 1] == ' ' and not (self.current[0], self.current[1] + 1) in self.visited\
and not (self.current[0], self.current[1] + 1) in self.queue:
self.queue.append((self.current[0], self.current[1] + 1))
elif self.maze[self.current[0]][self.current[1] + 1] == 'G':
return self.path
if self.maze[self.current[0] + 1][self.current[1]] == ' ' and not (self.current[0] + 1, self.current[1]) in self.visited\
and not (self.current[0] + 1, self.current[1]) in self.queue:
self.queue.append((self.current[0] + 1, self.current[1]))
elif self.maze[self.current[0] + 1][self.current[1]] == 'G':
return self.path
if self.maze[self.current[0]][self.current[1] - 1] == ' ' and not (self.current[0], self.current[1] - 1) in self.visited\
and not (self.current[0], self.current[1] - 1) in self.queue:
self.queue.append((self.current[0], self.current[1] - 1))
elif self.maze[self.current[0]][self.current[1] - 1] == 'G':
return self.path
self.visited.append((self.current[0], self.current[1]))
del self.queue[0]
self.path.append(self.queue[0])
As maze I use something like this:
############
# S #
##### ######
# #
######## ###
# #
## ##### ###
# G#
############
Which is stored in a matrix
What I eventually want is just the shortest path inside a list as output.

Since this is a coding assignment I'll leave the code to you and simply explain the general algorithm here.
You have a n by m grid. I am assuming this is is provided to you. You can store this in a two dimensional array.
Step 1) Create a new two dimensional array the same size as the grid and populate each entry with an invalid coordinate (up to you, maybe use None or another value you can use to indicate that a path to that coordinate has not yet been discovered). I will refer to this two dimensional array as your path matrix and the maze as your grid.
Step 2) Enqueue the starting coordinate and update the path matrix at that position (for example, update matrix[1,1] if coordinate (1,1) is your starting position).
Step 3) If not at the final coordinate, dequeue an element from the queue. For each possible direction from the dequeued coordinate, check if it is valid (no walls AND the coordinate does not exist in the matrix yet), and enqueue all valid coordinates.
Step 4) Repeat Step 3.
If there is a path to your final coordinate, you will not only find it with this algorithm but it will also be a shortest path. To backtrack, check your matrix at the location of your final coordinate. This should lead you to another coordinate. Continue this process and backtrack until you arrive at the starting coordinate. If you store this list of backtracked coordinates then you will have a path in reverse.

The main problem in your code is this line:
self.path.append(self.queue[0])
This will just keep adding to the path while you go in all possible directions in a BFS way. This path will end up getting all coordinates that you visit, which is not really a "path", because with BFS you continually switch to a different branch in the search, and so you end up collecting positions that are quite unrelated.
You need to build the path in a different way. A memory efficient way of doing this is to track where you come from when visiting a node. You can use the visited variable for that, but then make it a dictionary, which for each r,c pair stores the r,c pair from which the cell was visited. It is like building a linked list. From each newly visited cell you'll be able to find back where you came from, all the way back to the starting cell. So when you find the target, you can build the path from this linked list.
Some other less important problems in your code:
You don't check whether a coordinate is valid. If the grid is bounded completely by # characters, this is not really a problem, but if you would have a gap at the border, you'd get an exception
There is code repetition for each of the four directions. Try to avoid such repetition, and store recurrent expressions like self.current[1] - 1 in a variable, and create a loop over the four possible directions.
The variable running makes no sense: it never becomes False. Instead make your loop condition what currently is your next if condition. As long as the queue is not empty, continue. If the queue becomes empty then that means there is no path to the target.
You store every bit of information in self properties. You should only do that for information that is still relevant after the search. I would instead just create local variables for queue, visited, current, ...etc.
Here is how the code could look:
class Maze():
def __init__(self, str):
self.maze = str.splitlines()
def get_start(self):
row = next(i for i, line in enumerate(self.maze) if "S" in line)
col = self.maze[row].index("S")
return row, col
def main(self, r, c):
queue = [] # use a local variable, not a member
visited = {} # use a dict, key = coordinate-tuples, value = previous location
visited[(r, c)] = (-1, -1)
queue.append((r, c))
while len(queue) > 0: # don't use running as variable
# no need to use current; just reuse r and c:
r, c = queue.pop(0) # you can remove immediately from queue
if self.maze[r][c] == 'G':
# build path from walking backwards through the visited information
path = []
while r != -1:
path.append((r, c))
r, c = visited[(r, c)]
path.reverse()
return path
# avoid repetition of code: make a loop
for dx, dy in ((-1, 0), (0, -1), (1, 0), (0, 1)):
new_r = r + dy
new_c = c + dx
if (0 <= new_r < len(self.maze) and
0 <= new_c < len(self.maze[0]) and
not (new_r, new_c) in visited and
self.maze[new_r][new_c] != '#'):
visited[(new_r, new_c)] = (r, c)
queue.append((new_r, new_c))
maze = Maze("""############
# S #
##### ######
# #
######## ###
# #
## ##### ###
# G#
############""")
path = maze.main(*maze.get_start())
print(path)
See it run on repl.it

Related

I have a problem with python while loop that executes one line of code twice

I'm trying to solve a graph theory problem using breadth first traversal algorithm where the graph is represented as a grid.
The problem itself is from a local programming competition, so I can't share much details.
The input is provided for me (again, can't share it) in form of txt with some numerical values on each line that represent a grid size, position of start and finish points on that grid. Additionally, there are position of dangerous ares that I need to avoid.
I have no problem parsing the input. After that follows the code below. Some of the variables below are tuples because of the specific input and I thought that tuples could be good represention of x,y coordinate on a grid.
This is my code:
from collections import deque
GRID = 3 #size of the grid
START = (1, 1) #start position
END = (0, 0) #finish position
SAFE = False #boolean, tells if I could delete one item from noareas
noareas = [ (2, 1), (1, 2), (0, 1) ] #position of dangerous areas I have to avoid
def gridmover(grid, x, y, stop_areas):
dx = [-1, +1, 0, 0]
dy = [0, 0, +1, -1]
neighbours = []
for i in range(4):
xx = x + dx[i]
yy = y + dy[i]
if xx < 0 or yy < 0: continue
if xx >= grid or yy >= grid: continue
if (xx, yy) in stop_areas: continue
neighbours.append((xx,yy))
return neighbours
def breadthfirstsearch_custom(graph, start, end, stopareas, safe):
queue = deque([ start ])
visited = { start }
i = 0
while queue:
current = queue.popleft() #this line executes twice
path = [ current ]
i += 1
print(f"{i} This is current {current} and its type: {type(current)}")
if current == end:
return path
if safe is True: stopareas.pop(0)
neighbours = gridmover(graph, current[0], current[1], stopareas)
for neighbour in neighbours:
if neighbour not in visited:
queue.extend(neighbour)
path.extend(neighbour)
visited.add(neighbour)
return path
print(breadthfirstsearch_custom(GRID, START, END, noareas, SAFE))
Expected output should be a list of tuples that represent a path from start to finish.
This I see as an output:
1 This is current (1, 1) and its type: <class 'tuple'>
2 This is current 1 and its type: <class 'int'>
Traceback (most recent call last):
File "/Users/home/repos/kisaievla_github/fiks9/kolo1/t1_plaz/playgrid.py", line 52, in <module>
print(breadthfirstsearch_custom(GRID, START, END, noareas, PRKNO))
File "/Users/home/repos/kisaievla_github/fiks9/kolo1/t1_plaz/playgrid.py", line 42, in breadthfirstsearch_custom
neighbours = gridmover(graph, current[0], current[1], stopareas)
TypeError: 'int' object is not subscriptable
I understand the output above as while loop runs current = queue.popleft() twice. It pops a tuple from queue (desired behaviour) and then it pops an int from that tuple (not desired behaviour).
I send some time trying to find the issue. Hence the print statement in my code.
Strange thing is that when I have different values for START it behaves differently.
For example using the same code but with different variables
START = (0, 0)
END = (0, 0)
Gives this:
1 This is current (0, 0) and its type: <class 'tuple'>
[(0, 0)]
which is really what I want, since start is the same as end and the path to it is the coordinates.
I really don't know or see the solution here. Maybe it's not even an issue with the loop itself, but with popping.
Thanks for the answer/comments in advance.
The problem is in this code:
for neighbour in neighbours:
if neighbour not in visited:
queue.extend(neighbour)
path.extend(neighbour)
visited.add(neighbour)
neighbours is a list of tuples, returned from gridmover. Thus, neighbour will be a tuple.
When you do list.extend(tuple), that doesn't add the tuple to the list. Instead, it extends the list with the individual members of the tuple. If you do:
lst = [1,2,3]
lst.extend((4,5,6))
Then lst ends up as [1,2,3,4,5,6], NOT [1,2,3,(4,5,6)].
The simple answer is that you want append, not extend:
queue.append(neighbour)
path.append(neighbour)

Minesweeper AI labelling mines as safe spots

Background:
I have been working on the Minesweeper ai project for the HarvardX CS50AI online course for a few days. The goal is to implement AI for the minesweeper game. The problem set can be accessed here: https://cs50.harvard.edu/ai/2020/projects/1/minesweeper/
Implementation:
My task is to implement two classes, MinesweeperAI and the Sentence. Sentence class is a logical statement about a Minesweeper game that consists of a set of board cells and a count of the number of those cells which are mines. MinesweeperAI class is a main handler of AI.
Issue:
Although the program is running without any errors, the AI is making bad decisions, and thus, it is unable to complete the Minesweeper game successfully. From my observations, the AI is labelling potential mines as a safe space and thus, making suicidal runes.
Debugging
I have tried classical debugging, printing, even talking to myself about the code. For some reason, the AI is labelling statements that are mines as safe spaces - I can not detect the reason behind it. I have documented the code with comments, and I can not see any breakdown in implemented logic. However, there must be one - I am inserting the code below with some additional materials.
Sentence class, the logical representation of in-game knowledge:
class Sentence():
"""
Logical statement about a Minesweeper game
A sentence consists of a set of board cells,
and a count of the number of those cells which are mines.
"""
def __init__(self, cells, count):
self.cells = set(cells)
self.count = count
def __eq__(self, other):
return self.cells == other.cells and self.count == other.count
def __str__(self):
return f"{self.cells} = {self.count}"
def known_mines(self):
"""
Returns the set of all cells in self.cells known to be mines.
"""
# Because we are eliminating safe cells from the the statement, we are looking for statements
# that would contain number of cells that is equal (or smaller) than number of mines.
# Upon fulfilment of such condition, evaluated cells are known to be mines.
if len(self.cells) <= self.count:
return self.cells
else:
return None
def known_safes(self):
"""
Returns the set of all cells in self.cells known to be safe.
"""
# There is only one case when the cells are known to be "safes" - when the number of count is 0.
if self.count == 0:
return self.cells
else:
return None
def mark_mine(self, cell):
"""
Updates internal knowledge representation given the fact that
a cell is known to be a mine.
"""
# Marking mine implies two logical consequences:
# a) the number of counts must decrease by one (n - 1);
# b) the cell marked as mine must be discarded from the sentence (we keep track,
# only of the cells that are still unknown to be mines or "safes".
if cell in self.cells:
self.cells.discard(cell)
self.count -= 1
if self.count < 0: # this is a safeguard from any improper inference set forth.
self.count = 0
else:
pass
def mark_safe(self, cell):
"""
Updates internal knowledge representation given the fact that
a cell is known to be safe.
"""
# Marking "safe" implies one logical consequence:
# a) the cell marked as safe must be discarded from the sentence.
if cell in self.cells:
self.cells.discard(cell)
else:
pass
MinesweeperAI class, the primary AI module:
class MinesweeperAI():
"""
Minesweeper game player
"""
def __init__(self, height=8, width=8):
# Set initial height and width
self.height = height
self.width = width
# Keep track of which cells have been clicked on
self.moves_made = set()
# Keep track of cells known to be safe or mines
self.mines = set()
self.safes = set()
# List of sentences about the game known to be true
self.knowledge = []
def mark_mine(self, cell):
"""
Marks a cell as a mine, and updates all knowledge
to mark that cell as a mine as well.
"""
self.mines.add(cell)
for sentence in self.knowledge:
sentence.mark_mine(cell)
def mark_safe(self, cell):
"""
Marks a cell as safe, and updates all knowledge
to mark that cell as safe as well.
"""
self.safes.add(cell)
for sentence in self.knowledge:
sentence.mark_safe(cell)
def add_knowledge(self, cell, count):
"""
Called when the Minesweeper board tells us, for a given
safe cell, how many neighboring cells have mines in them.
This function should:
1) mark the cell as a move that has been made
2) mark the cell as safe
3) add a new sentence to the AI's knowledge base
based on the value of `cell` and `count`
4) mark any additional cells as safe or as mines
if it can be concluded based on the AI's knowledge base
5) add any new sentences to the AI's knowledge base
if they can be inferred from existing knowledge
"""
# 1) mark the cell as a move that has been made.
self.moves_made.add(cell)
# 2) mark the cell as safe. By this we are also updating our internal knowledge base.
self.mark_safe(cell)
# 3) add a new sentence to the AI's knowledge base based on the value of `cell` and `count`
sentence_prep = set()
# Sentence must include all the adjacent tiles, but do not include:
# a) the revealed cell itself;
# b) the cells that are known to be mines;
# c) the cell that are known to be safe.
for i in range(cell[0] - 1, cell[0] + 2):
for j in range(cell[1] - 1, cell[1] + 2): # Those two cover all the adjacent tiles.
if (i, j) != cell:
if (i, j) not in self.moves_made and (i, j) not in self.mines and (i, j) not in self.safes:
if 0 <= i < self.height and 0 <= j < self.width: # The cell must be within the game frame.
sentence_prep.add((i, j))
new_knowledge = Sentence(sentence_prep, count) # Adding newly formed knowledge to the KB.
self.knowledge.append(new_knowledge)
# 4) mark any additional cells as safe or as mines,
# if it can be concluded based on the AI's knowledge base
# 5) add any new sentences to the AI's knowledge base
# if they can be inferred from existing knowledge.
while True: # iterating knowledge base in search for new conclusions on safes or mines.
amended = False # flag indicates that we have made changes to the knowledge, new run required.
knowledge_copy = copy.deepcopy(self.knowledge) # creating copy of the database.
for sentence in knowledge_copy: # cleaning empty sets from the database.
if len(sentence.cells) == 0:
self.knowledge.remove(sentence)
knowledge_copy = copy.deepcopy(self.knowledge) # creating copy once again, without empty sets().
for sentence in knowledge_copy:
mines_check = sentence.known_mines() # this should return: a set of mines that are known mines or None.
safes_check = sentence.known_safes() # this should return: a set of safes that are known safes or None
if mines_check is not None:
for cell in mines_check:
self.mark_mine(cell) # marking cell as a mine, and updating internal knowledge.
amended = True # raising flag.
if safes_check is not None:
for cell in safes_check:
self.mark_safe(cell) # marking cell as a safe, and updating internal knowledge.
amended = True # raising flag.
# the algorithm should infer new knowledge,
# basing on reasoning: (A.cells - B.cells) = (A.count - B.count), if
# B is the subset of A.
knowledge_copy = copy.deepcopy(self.knowledge) # creating copy once again, updated checks.
for sentence_one in knowledge_copy:
for sentence_two in knowledge_copy:
if len(sentence_one.cells) != 0 and len(sentence_two.cells) != 0: # In case of the empty set
if sentence_one.cells != sentence_two.cells: # Comparing sentences (if not the same).
if sentence_one.cells.issubset(sentence_two.cells): # If sentence one is subset of sen_two.
new_set = sentence_two.cells.difference(sentence_one.cells)
if len(new_set) != 0: # if new set is not empty (in case of bug).
new_counts = sentence_two.count - sentence_one.count
if new_counts >= 0: # if the counts are equal or bigger than 0 (in case of bug).
new_sentence = Sentence(new_set, new_counts)
if new_sentence not in self.knowledge: # if the sentence is not already in
# the KB.
self.knowledge.append(new_sentence)
amended = True # raising flag.
if not amended:
break # If the run resulted in no amendments, then we can not make any additional amendments,
# to our KB.
def make_safe_move(self):
"""
Returns a safe cell to choose on the Minesweeper board.
The move must be known to be safe, and not already a move
that has been made.
This function may use the knowledge in self.mines, self.safes
and self.moves_made, but should not modify any of those values.
"""
for cell in self.safes:
if cell not in self.moves_made:
return cell
return None
def make_random_move(self):
"""
Returns a move to make on the Minesweeper board.
Should choose randomly among cells that:
1) have not already been chosen, and
2) are not known to be mines
"""
for i in range(self.height):
for j in range(self.width):
cell = (i, j)
if cell not in self.moves_made and cell not in self.mines:
return cell
return None
Documentation of the issue:
Documentation of the issue - the AI is making a safe move that it should now have labelled as the safe
Some comments:
Generally speaking, the cell is known to be safe when the sentence.count is zero (it means, that all the cells in the sentence are known to be "safes"). On the other hand, the cell is known as a mine, if the (len) of cells is equal to the sentence.count. The logic behind it is rather straightforward, still, I am missing something big when it comes to the implementation.
Thank you for all your help. Please do not be too harsh on my code - I am still learning, and to be honest, it's the first time when I am struggling hard with a piece of code that I have prepared. It's giving me little rest because I just can not crack down on what I am doing wrong. If there is something that I could provide (any more additional data) - please, just let me know!
Ok, after a lot debugging I found the root of the issue: When new knowledge is added via add_knowledge, the AI does only half account for cells it knows to be mines: It does not added those to the new Sentence, but one also needs to reduce the count by one for each already known cell:
for i in range(cell[0] - 1, cell[0] + 2):
for j in range(cell[1] - 1, cell[1] + 2): # Those two cover all the adjacent tiles.
if (i, j) != cell:
if (i, j) not in self.moves_made and (i, j) not in self.mines and (i, j) not in self.safes:
if 0 <= i < self.height and 0 <= j < self.width: # The cell must be within the game frame.
sentence_prep.add((i, j))
elif (i, j) in self.mines: # One of the neighbors is a known mine. Reduce the count.
count -= 1
new_knowledge = Sentence(sentence_prep, count) # Adding newly formed knowledge to the KB.
self.knowledge.append(new_knowledge)
This should now work (Unless there is another edge case somewhere)
Here a bit about my journey. I wrote these Tools to help with debugging:
def get_neighbours(size, x, y):
for i in range(x - 1, x + 2):
for j in range(y - 1, y + 2): # Those two cover all the adjacent tiles.
if (i, j) != (x, y):
if 0 <= i < size[0] and 0 <= j < size[1]:
yield i, j
class SimpleBoard:
def __init__(self, size, grid):
self.size = size
self.grid = grid
self.calc()
def calc(self):
for x in range(self.size[0]):
for y in range(self.size[1]):
if self.grid[x][y] != 9:
self.grid[x][y] = sum(1 for i, j in get_neighbours(self.size, x, y) if self.grid[i][j] == 9)
#classmethod
def random(cls, size, count):
self = cls(size, [[0] * size[1] for _ in range(size[0])])
options = list(product(range(size[0]), range(size[1])))
shuffle(options)
mines = options[:count]
for x, y in mines:
self.grid[x][y] = 9
self.calc()
return self
def build_ai_view(ai: MinesweeperAI, board: SimpleBoard):
out = []
for x in range(ai.height):
out.append(l :=[])
for y in range(ai.width):
cell = x,y
if cell in ai.mines:
assert cell not in ai.safes
l.append("X" if board.grid[x][y] == 9 else "%")
elif cell in ai.safes:
l.append(str(board.grid[x][y]) if cell in ai.moves_made else "_")
else:
l.append("?")
cells_to_sentence = defaultdict(list)
for i, sentence in enumerate(ai.knowledge):
for c in sentence.cells:
cells_to_sentence[c].append(sentence)
unique_groups = []
for c, ss in cells_to_sentence.items():
if ss not in unique_groups:
unique_groups.append(ss)
labels = "abcdefghijklmnopqrstuvxyz"
for (x, y), ss in cells_to_sentence.items():
i = unique_groups.index(ss)
l = labels[i]
assert out[x][y] == "?"
out[x][y] = l
for i, ss in enumerate(unique_groups):
out.append(l := [labels[i]])
if len(ss) > 1:
l.append("overlap of")
for s in ss:
if [s] not in unique_groups:
unique_groups.append([s])
l.append(labels[unique_groups.index([s])])
# l.extend(labels[unique_groups.index([s])] for s in ss)
else:
l.append(str(ss[0].count))
out.append([repr(ai)])
return "\n".join(map(str, out))
They might not be pretty code, but they work and display all relevant information from the perspective of the AI. I then used this together with the given failing case:
board = SimpleBoard((8, 8), [
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 9, 0, 0, 0, 9, 0, 0],
[0, 0, 0, 9, 0, 0, 0, 0],
[0, 0, 0, 9, 0, 0, 0, 0],
[0, 9, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 9, 0, 9, 0, 9, 0, 0],
])
and this simple loop:
pprint(board.grid)
start = next((x, y) for x in range(board.size[0]) for y in range(board.size[1]) if board.grid[x][y] == 0)
ai = MinesweeperAI(*board.size)
ai.add_knowledge(start, 0)
print(build_ai_view(ai, board))
while True:
target = ai.make_safe_move()
print(target)
x, y = target
if board.grid[x][y] == 9:
print("FOUND MINE", x, y)
break
else:
ai.add_knowledge((x, y), board.grid[x][y])
print(build_ai_view(ai, board))
to be able to backwards figure out at which point the AI starts to make false assumptions.
This came in multiple steps: figure out when the first % (e.g. wrongly marked mine) appears, figure out which Sentences lead to that conclusion, figure out which of those is wrong and finally figure out why that assumption is made.

How to check if a pointer can traverse a possible path given a fixed number of moves

I am trying to solve this problem on Hackerrank:
I have this input:
........#....#..#..#....#...#..#.#.#.#.#.#..#.....
..#..#..#.#....#..#.....#......#..##...........##.
.........#.###.##...#.....##......###............#
....##........#..#.#.#......#...#.##.......##.....
.................###...#.#...#......#.#.#.#.#...#.
.........#.....#...........##....#.#.#.##.....##..
.....#.##............#....#......#..#..#...##.....
.#.......###....#.#..##.##.#...##...#........#...#
..#.##..##..........#..........##.....##..........
#.#..##......#.#.#..##.###...#.........###..#...#.
.#..#..............#...#........#..#...#....#..#..
##..#..#........#....#........#...#.#......#.....#
#.#.......#.#..#...###..#..#.##...#.##.......#...#
#.#...#...#.....#....#......##.#.#.........#....#.
.#..........#......##..#....#....#.#.#..#..###....
#.#............#.##..#.##.##......###......#..#..#
.#..#.##...#.#......................#........#....
.....#....#.#..........##.#.#................#....
##............#.#......####...#.........#..##..#..
....#..##..##...#.........##..##....#..#.##...#...
.#........#...#..#...........#.###.....##.....##..
.......#..#..##...#..###.....#..##.........#......
...#......#..#...........###...............#......
...##.###.#.#....#...#..#.#.#....#....#.##.#...#..
..........#.......#..#..#...###....##.....#..#....
.............##.##.#.......#.#....#.......#..#..#.
.......#........#.....#....##...#...#.#...#.#.##..
.....#..#..#........#..#.....#...#.##.#....#...#..
....................#.#...#....###...###...##...#.
##.#.....##.....#..#.#.#...........#.#.##...#..#.#
#...........#....#.##...#.#.....#...#.....#.#.....
..#..##...#........#.##..#.....##.......#...#.#.#.
......#....#...##...........#..#.......#.##.......
......#..#..#.###..........#...#...........#..#...
....#.#..#..#.#.#...#.......#...#.##......#.......
....#.......#..#........#...#.#...#......#.......#
.#....##...#.#..#....#.#.##........#..#.#.........
#....#.......#..##......##...............#..#.##..
...#..##.......#.....#....#...#.#......#..##..###.
.....#...#...#...#...#...#..##...#..#.............
....##......#...#..#...#...#.#....#.....#..#.##...
...##.......#..##.....#........#.#....#...#.......
..#...#....#...#.###......#................#......
...#...###...#..##...###.....................#....
.....#....#....#...#.#.#.##....##......#....##....
...#.###...##.........#..........#.##.#.....#.....
##..#...#.........#.......#......##...........####
...###.#..........#.....#####........#..#.#.#...#.
...#..#.....#..##.##.#.....##...#...#.#.....#...##
.##.......#.##....#............#..................
#.....#.........#.#.........#..###....##...##.....
#....#.....#...#.....#.##...##...####........#....
#...........#..#...#........#.##..##..#...#.#.....
..#.#................#......###..##.#.#...##...#..
.#.#....#..#............#....#......#............#
..#..#...#.#.#...#...........#.......##.#...#.#...
#..........#.....#.....#......#.......#.#...##....
.......#...........#...........#....#............#
...####.#.....#.##.....#.......##.#..#......#.....
.#..#.....#..#......#.............#.#.#..##...#...
..#.#.#.........#...#..#.......#................##
.#..##.#.#...#.............#..#..........#..#...#.
....#........#......#...###..#.#..................
#..#..#.....#.#.#...##....##........#........#....
.....#.#.......##.......#.....#........#..##..#...
#.#.##........#..##.#..#.#...#........#.#......#..
....#.#.#.......#.##.##...##...#..#.###...#.#.#...
.....##.#....#........#....#.#........#.#.#.....#.
.....#..##..#.#....#.......#...#.#.###.........#.#
#.....#.##..#.......###.........#..##..#......##..
70 rows and the maxTime is 2244
This is my strategy but it works for some test cases:
import math
import collections
def reachTheEnd(grid, maxTime):
# Write your code here
grid = [i for i in grid]
yes = 'Yes'
no = 'No'
counter = 0
for i in grid:
counter += i.count('.')
if maxTime <= counter:
return yes
elif i != i[::-1]:
return no
else:
return no
print(counter)
This is a BFS problem but I couldn't figure out the logic. I appreciate all your help.
The idea of breadth-first search is that you (1) don't visit the same node twice, and (2) continuously maintain a list of nodes you haven't visited, split up by timeslice. Eventually, you visit all visitable nodes, and for any node you do visit, you visit it in as few steps as possible.
In the following algorithm, we create a cache to satisfy (1) and a queue to satisfy (2). Each timeslice, we examine each element of the queue, and replace the queue entirely with a new queue, composed of elements discovered during that timeslice. Whenever we discover a new element, we add it to the cache along with the timeslice on which it was first found - this, then, must be the quickest route to that new element.
If we either run out of time or run out of new cells to explore before reaching the destination, then we fail. Otherwise, we succeed. We can check that by simply encoding our exit conditions and checking if we've visited the destination by the time we exit the while loop.
def reachTheEnd(grid, maxTime):
# mark what the coordinates of the destination are
destination = (len(grid) - 1, len(grid[-1]) - 1)
# initialize
# - a step counter
# - a cache of visited cells on the grid
# - a queue of not-yet-visited cells that are adjacent to visited cells
counter = 0
cache = {(0, 0): 0}
queue = [(0, 0)]
# perform breadth-first search on the current queue, moving forward one
# timeslice. On each timeslice, we take one 'step' forward in any direction
# towards a newly-accessible tile on the grid.
# our 'exit' conditions are
# - we run out of time
# - there is no path to the end of the maze
# - we reach the end
while counter < maxTime and len(queue) > 0 and destination not in cache:
counter += 1
new_queue = []
for (x, y) in queue:
# check adding to path in all directions (up, down, left, right)
# If the step is
# - not out-of-bounds
# - not a wall
# - not already visited
# then add it to the cache with the current step count, as this is
# is the most quickly we can reach it. Also add it to the queue of
# cells to investigate on the next timeslice.
for (dx, dy) in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
(nx, ny) = (x + dx, y + dy)
if 0 <= nx < len(grid) \
and 0 <= ny < len(grid[nx]) \
and grid[nx][ny] == '.' \
and (nx, ny) not in cache:
cache[(nx, ny)] = counter
new_queue.append((nx, ny))
queue = new_queue
# by the time the loop exits, either we've failed to reach the end,
# or the end is in the cache with the quickest possible path to it.
if destination in cache:
return "Yes"
else:
return "No"
A possible optimization would be to move the "have we reached destination yet" to inside the innermost for loop, which would save processing the rest of the queue on that timeslice. However, this would make the code slightly more complicated (and thus less useful for explaining the concept of BFS) and provide only a minimal time save.
Note that for the big 70x50 grid you've provided, there's no way to actually reach the lower-right square (it's a small island surrounded by walls). It can reach cell (67, 49) by timeslice 117, which is as close as it gets, but can't get around the wall.

LeetCode: Flood Fill, Recursion in For Loop stuck in endless loop

I was working on this specific LeetCode problem and I encountered a problem where I would be stuck recursing. The way I understand it, if an input type is mutable, the input should be pass by reference, so they should be referencing the same thing. Can someone explain how my method breaks? I really want to try solving this problem using recursion, but I don't understand how to do it using my method. My code first finds north, east,south,west, and then determines if they are valid. It then determines if among those directions if they have the same count as the original node.
Of those that have the same count as the original node, I need to recurse on those and repeat the process until all nodes have the value of newColor
https://leetcode.com/problems/flood-fill/
class Solution:
def floodFill(self, image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int]]:
top = (sr-1, sc)
down = (sr+1, sc)
left = (sr, sc-1)
right = (sr, sc+1)
# Possible Directions
posDirec = [direc for direc in [top,down,left,right] if direc[0] >=0 and direc[1] >=0 and direc[0] < len(image) and direc[1] < len(image[0])]
# Neighbors that we can traverse
posNeigh = [e for e in posDirec if image[e[0]][e[1]] == image[sr][sc]]
image[sr][sc] = newColor
# print(image, '\n')
print(len(posNeigh), posNeigh, image)
if len(posNeigh) == 0:
pass
else:
for neigh in posNeigh: #top, down,left, right of only valids
self.floodFill(image, neigh[0], neigh[1], newColor)
return image
At the very end, my program should return the image. I want to return the image at the end, however, my code ends up stuck in recursion
Take a look at the following line:
# Neighbors that we can traverse
posNeigh = [e for e in posDirec if image[e[0]][e[1]] == image[sr][sc]]
This condition fails to account for the possibility that image[e[0]][e[1]] has already been filled in with newColor, resulting in an infinite loop between filled cells and a stack overflow.
If we change it to
posNeigh = [
e for e in posDirec
if image[e[0]][e[1]] == image[sr][sc]
and image[e[0]][e[1]] != newColor # <-- added
]
we can make sure we're not revisiting previously-filled areas.
Given that the list comprehensions have grown quite unwieldy, you might consider a rewrite:
def floodFill(self, image, sr, sc, new_color):
target_color = image[sr][sc]
image[sr][sc] = new_color
for y, x in ((sr + 1, sc), (sr, sc - 1), (sr, sc + 1), (sr - 1, sc)):
if y >= 0 and x >= 0 and y < len(image) and x < len(image[0]) and \
image[y][x] != new_color and image[y][x] == target_color:
self.floodFill(image, y, x, new_color)
return image
A mutable input does not pass by reference. The way I see it, solving it using recursion is not possible. Try an iterative solution.

Python - speed up pathfinding

This is my pathfinding function:
def get_distance(x1,y1,x2,y2):
neighbors = [(-1,0),(1,0),(0,-1),(0,1)]
old_nodes = [(square_pos[x1,y1],0)]
new_nodes = []
for i in range(50):
for node in old_nodes:
if node[0].x == x2 and node[0].y == y2:
return node[1]
for neighbor in neighbors:
try:
square = square_pos[node[0].x+neighbor[0],node[0].y+neighbor[1]]
if square.lightcycle == None:
new_nodes.append((square,node[1]))
except KeyError:
pass
old_nodes = []
old_nodes = list(new_nodes)
new_nodes = []
nodes = []
return 50
The problem is that the AI takes to long to respond( response time <= 100ms)
This is just a python way of doing https://en.wikipedia.org/wiki/Pathfinding#Sample_algorithm
You should replace your algorithm with A*-search with the Manhattan distance as a heuristic.
One reasonably fast solution is to implement the Dijkstra algorithm (that I have already implemented in that question):
Build the original map. It's a masked array where the walker cannot walk on masked element:
%pylab inline
map_size = (20,20)
MAP = np.ma.masked_array(np.zeros(map_size), np.random.choice([0,1], size=map_size))
matshow(MAP)
Below is the Dijkstra algorithm:
def dijkstra(V):
mask = V.mask
visit_mask = mask.copy() # mask visited cells
m = numpy.ones_like(V) * numpy.inf
connectivity = [(i,j) for i in [-1, 0, 1] for j in [-1, 0, 1] if (not (i == j == 0))]
cc = unravel_index(V.argmin(), m.shape) # current_cell
m[cc] = 0
P = {} # dictionary of predecessors
#while (~visit_mask).sum() > 0:
for _ in range(V.size):
#print cc
neighbors = [tuple(e) for e in asarray(cc) - connectivity
if e[0] > 0 and e[1] > 0 and e[0] < V.shape[0] and e[1] < V.shape[1]]
neighbors = [ e for e in neighbors if not visit_mask[e] ]
tentative_distance = [(V[e]-V[cc])**2 for e in neighbors]
for i,e in enumerate(neighbors):
d = tentative_distance[i] + m[cc]
if d < m[e]:
m[e] = d
P[e] = cc
visit_mask[cc] = True
m_mask = ma.masked_array(m, visit_mask)
cc = unravel_index(m_mask.argmin(), m.shape)
return m, P
def shortestPath(start, end, P):
Path = []
step = end
while 1:
Path.append(step)
if step == start: break
if P.has_key(step):
step = P[step]
else:
break
Path.reverse()
return asarray(Path)
And the result:
start = (2,8)
stop = (17,19)
D, P = dijkstra(MAP)
path = shortestPath(start, stop, P)
imshow(MAP, interpolation='nearest')
plot(path[:,1], path[:,0], 'ro-', linewidth=2.5)
Below some timing statistics:
%timeit dijkstra(MAP)
#10 loops, best of 3: 32.6 ms per loop
The biggest issue with your code is that you don't do anything to avoid the same coordinates being visited multiple times. This means that the number of nodes you visit is guaranteed to grow exponentially, since it can keep going back and forth over the first few nodes many times.
The best way to avoid duplication is to maintain a set of the coordinates we've added to the queue (though if your node values are hashable, you might be able to add them directly to the set instead of coordinate tuples). Since we're doing a breadth-first search, we'll always reach a given coordinate by (one of) the shortest path(s), so we never need to worry about finding a better route later on.
Try something like this:
def get_distance(x1,y1,x2,y2):
neighbors = [(-1,0),(1,0),(0,-1),(0,1)]
nodes = [(square_pos[x1,y1],0)]
seen = set([(x1, y1)])
for node, path_length in nodes:
if path_length == 50:
break
if node.x == x2 and node.y == y2:
return path_length
for nx, ny in neighbors:
try:
square = square_pos[node.x + nx, node.y + ny]
if square.lightcycle == None and (square.x, square.y) not in seen:
nodes.append((square, path_length + 1))
seen.add((square.x, square.y))
except KeyError:
pass
return 50
I've also simplified the loop a bit. Rather than switching out the list after each depth, you can just use one loop and add to its end as you're iterating over the earlier values. I still abort if a path hasn't been found with fewer than 50 steps (using the distance stored in the 2-tuple, rather than the number of passes of the outer loop). A further improvement might be to use a collections.dequeue for the queue, since you could efficiently pop from one end while appending to the other end. It probably won't make a huge difference, but might avoid a little bit of memory usage.
I also avoided most of the indexing by one and zero in favor of unpacking into separate variable names in the for loops. I think this is much easier to read, and it avoids confusion since the two different kinds of 2-tuples had had different meanings (one is a node, distance tuple, the other is x, y).

Categories

Resources