Reputation: 1150
I am struggling to implement an algorithm that resolves a 2D maze array by only changing a direction once a wall or another obstacle is hit.
What it needs to do is, given the following array (where x
is the start, 1
is an obstacle, g
is the goal) find the shortest path by only hitting obstacles first (no changing direction of movement unless obstacle/wall is reached).
[[1, 1, 1, 1, 1]
[1, x, 0, 0, 1]
[1, 0, 0, 0, g]
[1, 1, 1, 1, 1]]
The solution should be:
[(1,1), (2,1), (2,4)]
In the example above, it moves only next to the maze walls, but that's only because the example is very small. All in all, it should only move in the 4 directions and not change its course once it starts a direction until an obstacle is met. Here is a more visual example:
I managed to find the following code that gets the length of the shortest path but does not display the path itself. Any help to get that would be very appreciated.
def shortestDistance(self, maze: List[List[int]], start: List[int], destination: List[int]):
start, destination = tuple(start), tuple(destination)
row, col = len(maze), len(maze[0])
def neighbors(maze, node):
temp = []
used = set()
used.add(node)
for dx, dy in [(-1, 0), (0, 1), (0, -1), (1, 0)]:
(x, y), dist = node, 0
while 0 <= x + dx < row and 0 <= y + dy < col and maze[x + dx][y + dy] == 0:
x += dx
y += dy
dist += 1
if (x, y) not in used:
temp.append((dist, (x, y)))
return temp
heap = [(0, start)]
visited = set()
while heap:
dist, node = heapq.heappop(heap)
if node in visited: continue
if node == destination:
return dist
visited.add(node)
for neighbor_dist, neighbor in neighbors(maze, node):
heapq.heappush(heap, (dist + neighbor_dist, neighbor))
return -1
Upvotes: 3
Views: 2622
Reputation: 17156
Issue:
Algorithm & Code
Key Algorithm Modification from normal wall following
Code
# Modification of https://github.com/nerijus-st/Maze-Solving/blob/master/left%20hand%20rule.py
import numpy as np
import heapq
class Maze():
# Movement directions
directions = ["N", "E", "S", "W"]
def __init__(self, maze):
assert isinstance(maze, np.ndarray) # maze should be 2D numpy array
self.maze = maze
self.m, self.n = maze.shape
def solve_maze(self, start, goal):
"""
N
W E
S
UP (N) - get_neighbours()['N']
RIGHT (E) - get_neighbours()['E']
DOWN (S) - get_neighbours()['S']
LEFT (W) - get_neighbours()['W']
maze 2D Numpy array
start tuple for stating position
goal tuple for destination
Strategy: Keeps going in same direction until a wall is reached. Then try
form branches for both left and right directions (i.e. search both)
"""
# Try all starting directions
heap = [(0, 0, facing, [start]) for facing in Maze.directions]
heapq.heapify(heap)
while heap:
dist, changes, facing, path = heapq.heappop(heap)
changes = -changes # Negative to make earlier since using min heap
if path[-1] == goal:
return self.compress_path(path)
self.x, self.y = path[-1] # Set current position in maze
front_wall = self.get_front_wall(facing) # Coordinates next position in front
if front_wall and not self.maze[front_wall] and not front_wall in path: # if Clear in front
# Front direction clear
heapq.heappush(heap, (dist+1, -changes, facing, path + [front_wall]))
elif len(path) > 1:
# Try to left
left_wall = self.get_left_wall(facing) # Coordinates to the left
if left_wall and not self.maze[left_wall] and not left_wall in path: # if clear to the left
left_facing = self.rotate_facing(facing, "CCW") # Rotate left (i.e. counter clockwise)
heapq.heappush(heap, (dist+1, -(changes+1), left_facing, path + [left_wall]))
# Try to the right
right_wall = self.get_right_wall(facing) # Coordinates to the right
if right_wall and not self.maze[right_wall] and not right_wall in path: # if Clear to the right
right_facing = self.rotate_facing(facing, "CW") # Rotate right (i.e. clockwise)
heapq.heappush(heap, (dist+1, -(changes+1), right_facing, path + [right_wall]))
def compress_path(self, path):
if not path:
return
if len(path) < 3:
return path
result = [path[0]]
for i, p in enumerate(zip(path, path[1:])):
direction = self.get_direction(*p)
if i == 0:
prev_direction = direction
result.append(p[1][:])
continue
if len(result) > 2:
if prev_direction == direction:
result[-1] = p[1][:]
else:
result.append(p[1][:])
else:
result.append(p[1][:])
prev_direction = direction
return result
def get_neighbours(self):
' Neighbors of current position '
x, y = self.x, self.y
result = {}
result['N'] = (x-1, y) if x + 1 < self.m else None
result['S'] = (x+1, y) if x-1 >= 0 else None
result['E'] = (x, y+1) if y + 1 < self.n else None
result['W'] = (x, y-1) if y -1 >= 0 else None
return result
def get_direction(self, point1, point2):
x1, y1 = point1
x2, y2 = point2
if y1 == y2 and x1 - 1 == x2:
return 'N'
if y1 == y2 and x1 + 1 == x2:
return 'S'
if x1 == x2 and y1 + 1 == y2:
return 'E'
if x1 == x2 and y1 - 1 == y2:
return 'W'
def get_left_wall(self, facing):
if facing == "N":
return self.get_neighbours()['W'] # cell to the West
elif facing == "E":
return self.get_neighbours()['N']
elif facing == "S":
return self.get_neighbours()['E']
elif facing == "W":
return self.get_neighbours()['S']
def get_right_wall(self, facing):
if facing == "N":
return self.get_neighbours()['E'] # cell to the East
elif facing == "E":
return self.get_neighbours()['S']
elif facing == "S":
return self.get_neighbours()['W']
elif facing == "W":
return self.get_neighbours()['N']
def get_front_wall(self, facing):
return self.get_neighbours()[facing]
def rotate_facing(self, facing, rotation):
facindex = Maze.directions.index(facing)
if rotation == "CW":
if facindex == len(Maze.directions) - 1:
return Maze.directions[0]
else:
return Maze.directions[facindex + 1]
elif rotation == "CCW":
if facindex == 0:
return Maze.directions[-1]
else:
return Maze.directions[facindex - 1]
Test
Test 1
maze = [[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 0],
[1, 1, 1, 1, 1]]
M = Maze(np.array(maze))
p = M.solve_maze((1,1), (2,3))
print('Answer:', p)
# Output: Answer: [(1, 1), (2, 1), (2, 3)]
Test 2
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1],
[1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
M = Maze(np.array(maze))
p = M.solve_maze((1,1), (23, 23))
print('Answer:', p)
# Output: Answer: [(1, 1), (1, 2), (1, 9), (3, 9), (3, 12), (5, 12), (5, 10), (7, 10), (7, 9), (17, 9), (17, 10), (23, 10), (23, 23)]
Upvotes: 2
Reputation: 71451
You can use a recursive generator function that at every step either continues to proceed on the same direction or changes course by finding other possible directions when it hits a wall:
graph = [[1, 1, 1, 1, 1], [1, 'x', 0, 0, 1], [1, 0, 0, 0, 'g'], [1, 1, 1, 1, 1]]
d_f = [lambda x, y:(x+1, y), lambda x, y:(x+1, y+1), lambda x, y:(x, y+1), lambda x, y:(x-1, y), lambda x, y:(x-1, y-1), lambda x, y:(x, y-1)]
def navigate(coord, f = None, path = [], s = []):
if graph[coord[0]][coord[-1]] == 'g':
yield path+[coord]
elif f is None:
for i, _f in enumerate(d_f):
if graph[(j:=_f(*coord))[0]][j[-1]] != 1 and j not in s:
yield from navigate(j, f = _f, path=path+[coord], s = s+[coord])
else:
if graph[(j:=f(*coord))[0]][j[-1]] == 1:
yield from navigate(coord, f = None, path=path, s = s)
else:
yield from navigate(j, f = f, path=path, s = s+[coord])
start = [(j, k) for j, a in enumerate(graph) for k, b in enumerate(a) if b == 'x']
r = min(navigate(start[0]), key=len)
Output:
[(1, 1), (2, 1), (2, 4)]
Upvotes: 0
Reputation: 46
You could interpret it as a graph problem. Generate a graph with all possible movements which then are the edeges and the possbile positions being the nodes. The edges should be marked with their length (or weight) (in your case maybe 6 blocks for example). I guess the ball starts somewhere thus the question is (while thinking of graphs): Whats is the shortest path to a node u form a node v.
Therfore you can use the Bellman–Ford algorithm. I will not explain it because wikipedia does a better job with that. There are some more algorithms which solve you problem like the Dijkstra's algorithm if you don't like that one.
Upvotes: 0