feedy
feedy

Reputation: 1150

How to find the shortest path in 2D maze array, by only hitting walls

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:

enter image description here

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

Answers (3)

DarrylG
DarrylG

Reputation: 17156

Issue:

  • Usual breadth first search is too slow to solve the size of maze poster requires i.e. 24 x 24.

Algorithm & Code

Key Algorithm Modification from normal wall following

  • Continues to step in the same direction until we hit a wall
  • Create new branching paths to left and right of current direction when hit a wall in current direction
  • Use heap to focus on extending paths with minimum distances and number of direction changes

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

Ajax1234
Ajax1234

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

tom2208
tom2208

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

Related Questions