I'm having trouble solving a Google Foobar question involving path-finding. My solution fails 2 test cases, the inputs and outputs of which are hidden.
The prompt:
You have maps of parts of the space station, each starting at a prison exit and ending at the door to an escape pod. The map is represented as a matrix of 0s and 1s, where 0s are passable space and 1s are impassable walls. The door out of the prison is at the top left (0,0) and the door into an escape pod is at the bottom right (w-1,h-1).
Write a function answer(map) that generates the length of the shortest path from the prison door to the escape pod, where you are allowed to remove one wall as part of your remodeling plans. The path length is the total number of nodes you pass through, counting both the entrance and exit nodes. The starting and ending positions are always passable (0). The map will always be solvable, though you may or may not need to remove a wall. The height and width of the map can be from 2 to 20. Moves can only be made in cardinal directions; no diagonal moves are allowed.
Test cases
Inputs:
(int) maze = [[0, 1, 1, 0], [0, 0, 0, 1], [1, 1, 0, 0], [1, 1, 1, 0]]
Output:
(int) 7
Inputs:
(int) maze = [[0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1], [0, 0, 0, 0, 0, 0]]
Output:
(int) 11
My code:
from queue import PriorityQueue
# Grid class
class Grid:
# Initialized with dimensions to check later if all neighbor points are actually within the graph
def __init__(self, width, height):
self.width = width
self.height = height
self.walls = []
self.weights = {}
self.wall_count = 0
# Find the cost of a certain destination node
# Cost is reported as a tuple to account for going across a wall: (# of moves through a wall, # of normal moves)
def cost(self, from_node, to_node):
if to_node in self.walls:
return self.weights.get(to_node, (1, 0))
else:
return self.weights.get(to_node, (0, 1))
# Check if the location is actually within the graph
def in_bounds(self, id):
(x, y) = id
return 0 <= x < self.width and 0 <= y < self.height
# Find the adjacent nodes of a node (ie. the places it can go to)
# Filters out any result which isn't on the graph using self.in_bounds
def neighbors(self, id):
(x, y) = id
results = [(x+1, y), (x, y-1), (x-1, y), (x, y+1)]
if (x + y) % 2 == 0: results.reverse() # aesthetics
results = filter(self.in_bounds, results)
return results
# Find the dimensions of the 2D list by finding the lengths of the outer and inner lists
def dimensions_2d(xs):
width = len(xs)
height = len(xs[0])
return (width, height)
# Returns all the positions of an element in a 2D list
# In this case it's used to find all walls (occurences of 1) to pass to the Grid object
def index_2d(xs, v):
results = [(x, y) for y, ls in enumerate(xs) for x, item in enumerate(ls) if item == v]
return results
# Djikstra search algorithm; mistakenly named "a_star" before
# Returns both a dictionary of "destination" locations to "start" locations (tuples) as well as a dictionary of the calculated cost of each location on the grid
def djikstra_search(graph, start, goal):
# Priority Queue to select nodes from
frontier = PriorityQueue()
# Place our starting cost in
frontier.put(start, (0, 0))
came_from = {}
cost_so_far = {}
came_from[start] = None
cost_so_far[start] = (0, 0)
while not frontier.empty():
# Get the element with the highest priority from the queue
current = frontier.get()
if current == goal:
break
# For every neighbor of the selected node
for next in graph.neighbors(current):
# The new cost of the neighbor node is current cost plus cost of this node - (1, 0) if it goes through a wall, (0, 1) otherwise
new_cost = (cost_so_far[current][0] + graph.cost(current, next)[0], cost_so_far[current][1] + graph.cost(current, next)[1])
# If the node has not cost currently
# OR if the number of walls traveled through is less than the current cost
# AND if the number of normal steps taken is less than or the same as the current number
if next not in cost_so_far or (new_cost[0] < cost_so_far[next][0] and sum(new_cost) <= sum(cost_so_far[next])):
# Record it in both the cost and came_from dicts
cost_so_far[next] = new_cost
# Place the cost in the queue
priority = new_cost
frontier.put(next, priority)
came_from[next] = current
return came_from, cost_so_far
# Find the length of the calculated path
# Using the returned list of edges from djikstra_search, move backwards from the target end and increment the length until the start element is reached
def path(grid, start, end):
# Perform the search
path = djikstra_search(grid, start, end)
search = path[0]
# If the end element's cost travels through more than 1 wall return 0
if path[1].get(end)[0] > 1:
return 0
# Otherwise move backwards from the end element and increment length each time
# Once the start element has been reached, we have our final length
length = 1
last = end
while last != start:
last = search.get(last)
length += 1
return length
# The "main" function
def answer(maze):
# Find all occurences of walls (1) in the 2D list
walls = index_2d(maze, 1)
# Find the x and y dimensions of the maze (required for the Grid object)
dims = dimensions_2d(maze)
# Create a new grid with our found dimensions
grid = Grid(dims[0], dims[1])
# The start point will always be at (0,0) and the end will always be at the bottom-right so we define those here
start = (0, 0)
end = (dims[0] - 1, dims[1] - 1)
# the walls variable's locations are flipped, so reverse each of them to get the right wall positions
grid.walls = [(y, x) for x, y in walls]
# Return the length
return path(grid, start, end)
In my own testing (grids up to 7x7) this solution seems to work without problems.
Any help (or failing cases) would be much appreciated!