Звездный алгоритм с возможностью удаления N препятствий - PullRequest
0 голосов
/ 13 января 2020

Чтобы сохранить четкость кода, я начал с типичных головоломок. Я нашел реализацию алгоритма A *. Следующим шагом будет его адаптация, чтобы можно было устранить N препятствий, чтобы найти оптимальный путь. Если лабиринт представлен, как показано ниже, где 1 представляют препятствия:

[[0, 1, 1, 0], 
 [0, 0, 0, 0], 
 [1, 1, 1, 0], 
 [1, 0, 1, 0]]

Реализация A *: import heapq

class Cell:
    def __init__(self,x,y,passable):
        self.passable = passable
        self.x = x
        self.y = y
        self.g = 0
        self.h = 0
        self.f = 0
        self.parent = None

    def __repr__(self):
        return 'Cell((%s, %s), %s)' % (self.x, self.y, self.passable) 

class Maze:
    def __init__(self,maze_map):
        self.maze_map = maze_map
        self.cells = []
        self.maze_height = None
        self.maze_width = None
        self.cells_to_visit = []
        heapq.heapify(self.cells_to_visit)
        self.cells_visited = set()

    def initialize_cells(self):
        self.maze_height = len(self.maze_map)
        self.maze_width = len(self.maze_map[0])
        door_start = (0,0)
        door_end = (self.maze_height-1,self.maze_width-1)

        for h in range(self.maze_height):
            for w in range(self.maze_width):
                if self.maze_map[h][w] == 0:
                    passable = True
                else:
                    passable = False
                self.cells.append(Cell(w,h,passable))

        self.start = self.get_cell(*door_start)
        self.end = self.get_cell(*door_end)

    def calculate_heuristic(self,cell):
        cost = 10
        return cost * (abs(cell.x - self.end.x) + abs(cell.y - self.end.y))

    def get_cell(self, x, y):
        return self.cells[x * self.maze_height + y]

    def get_path(self):
        cell = self.end
        path = [(cell.x, cell.y)]
        while cell.parent is not self.start:
            cell = cell.parent
            path.append((cell.x, cell.y))

        path.append((self.start.x, self.start.y))
        path.reverse()
        return path

    def update_cell(self, adj, cell):
        adj.g = cell.g + 10
        adj.h = self.calculate_heuristic(adj)
        adj.parent = cell
        adj.f = adj.h + adj.g

    def get_adjacent_cells(self,cell):
        cells = []
        if cell.x < self.maze_width-1:
            cells.append(self.get_cell(cell.x+1, cell.y))
        if cell.y > 0:
            cells.append(self.get_cell(cell.x, cell.y-1))
        if cell.x > 0:
            cells.append(self.get_cell(cell.x-1, cell.y))
        if cell.y < self.maze_width-1:
            cells.append(self.get_cell(cell.x, cell.y+1))
        return cells

    def solve(self):
        heapq.heappush(self.cells_to_visit, self.start)
        while len(self.cells_to_visit):            
            cell = heapq.heappop(self.cells_to_visit)
            self.cells_visited.add(cell)

            if cell is self.end:
                return self.get_path()

            adj_cells = self.get_adjacent_cells(cell)
            for adj_cell in adj_cells:
                if adj_cell not in self.cells_visited:
                    if (adj_cell.f, adj_cell) in self.cells_to_visit:
                        if adj_cell.g > cell.g + 10:
                            self.update_cell(adj_cell, cell)
                    else:
                        self.update_cell(adj_cell, cell)
                        heapq.heappush(self.cells_to_visit, adj_cell)

if __name__ == "__main__":
    m = [[0, 1, 1, 0], 
         [0, 0, 1, 0], 
         [1, 1, 1, 0], 
         [1, 0, 1, 0]]

    N = 1 #allowed to remove 1 obstacle
    maze = Maze(map,N)
    maze.initialize_cells()
    maze.solve()

    m = [[0, 1, 1, 0], 
         [0, 0, 1, 0], 
         [1, 1, 1, 1], 
          [1, 0, 1, 0]]

    N = 2 #allowed to remove 2 obstacles
    maze = Maze(map,N)
    maze.initialize_cells()
    maze.solve()

Начиная с возможности удаления одного препятствия, насколько легко сделать его обобщенным на N? Кроме того, как реализовать такую ​​вещь?

Заранее спасибо.

1 Ответ

0 голосов
/ 13 января 2020

Во-первых, я хотел бы сказать, что если у вас есть информация о проблеме, такая, что «есть несколько препятствий, которые необходимо устранить» (имеется в виду, что препятствия редки), алгоритм, который работает только с учетом препятствий, может оказаться Быстрее. Но я дам общее решение ( Я превратил проблему в задачу поиска пути ) и несколько советов.

1 - В играх используется хитрость (по крайней мере, я знаю это по игры). Помните, что в алгоритме A * есть затраты. Например, в играх, если вы хотите, чтобы водный путь был более дорогостоящим, чем наземный, вы увеличиваете значение «g» в акваториях по сравнению с земными участками. Хитрость заключается в умном использовании этой функции стоимости. В некоторых играх, когда вы пытаетесь переместить персонажа из точки А в точку Б, и между точками нет пустого пути, персонаж не просто стоит там, а идет к точке Б столько, сколько может. Это делается с помощью вместо того, чтобы полностью отбрасывать непроходимые области, вы назначаете им очень высокую стоимость . В результате персонаж начинает идти к тончайшему препятствию между двумя точками, вместо того чтобы стоять и ничего не делать. Если мы примем эту идею к вашей проблеме, проблема в основном превратится в «найти путь с наименьшим количеством 1 с». (Строки 46, 106 и 139 в моем коде)

2 - Я обычно ничего не делаю в python, поэтому я оборачиваю операции с кучей в классе PriorityQueue (я взял этот код класса откуда-то, а не конечно где). Но я думаю, что управлять этими вещами проще, также основной корпус A * выглядит ясным.

3 - Вам не нужно писать отдельный оператор if для разных направлений (вправо, вверх, влево, вниз) , Да, есть аргумент, что это быстрее, но если вам действительно нужна такая скорость, вы можете не использовать python. (строки 92-93 и 122-124, чтобы увидеть использование)

4- Я использовал манхэттенское расстояние в качестве heuristi c, предполагая, что только 4 кардинальных направления разрешены для перемещения, если разрешены диагональные перемещения, это должно быть изменено на евклидово расстояние и необходимо учитывать 4 других направления.

import time
import random


#  use whichever data structure you like
import heapq


class Node():

    #  maybe calculate h value inside function instead of taking as arg
    def __init__(self, parent_node, row, col, g, h):
        self.parent_node = parent_node
        self.row = row
        self.col = col
        self.g = g
        self.h = h

    def __lt__(self, other):
        return self.g + self.h < other.g + other.h




class PriorityQueue: 
    def __init__(self):
        self.elements = []

    def empty(self):
        return len(self.elements) == 0

    def put(self, item, priority):
        heapq.heappush(self.elements, (priority, item))

    def get(self):
        return heapq.heappop(self.elements)[1]


class Maze:
    def __init__(self, maze_map):
        self.maze_map = maze_map
        self.maze_height = len(self.maze_map)
        self.maze_width = len(self.maze_map[0])

        #  this number must be large enough compared to your maze size
        self.OBSTACLE_COST = 1000


    def print_maze(self):
        for row in self.maze_map:
            for col in row:
                print(col, end='')
            print("")


    def calculate_h(self, row, col):
        #  lets use manhattan distance, since we do not go diagonally
        #convert to euclidian if diagonal movement is allowed
        return abs(row - end_row) + abs(col - end_col)

    def solve(self, start_row, start_col, goal_row, goal_col):
        self.start_row = start_row
        self.start_col = start_col
        self.end_row = end_row
        self.end_col = end_col


        #  initialize result variables
        found_path = []
        found_path_cost = 0



        #  create root A* node
        h_initial = self.calculate_h(start_row, start_col)
        s0 = Node(None, start_row, start_col, 0, h_initial)


        #  create our priority queue and put root node in it
        Q = PriorityQueue()
        Q.put(s0, s0.h+0)


        #  keep visited/closed set, (so we are performing graph search) 
        closed_set = {}


        #  these will be useful
        #these numbers correspond to RIGHT,UP,LEFT,DOWN respectively
        #for right change in y=0 and change in x = 1
        #...
        dy = [0, -1, 0, 1]
        dx = [1, 0, -1, 0]
        while (not Q.empty()):
            #  pop a node from the priority queue
            s = Q.get()

            if (s.row == self.end_row and s.col == self.end_col):
                #  solved, obtain the path by following parents of nodes
                current_node = s
                while current_node.parent_node != None:
                    found_path.append(current_node)
                    if (self.maze_map[current_node.row][current_node.col] == 0):
                        found_path_cost += 1
                    else:
                        found_path_cost += self.OBSTACLE_COST

                    current_node = current_node.parent_node

                found_path.reverse()
                break


            #  expand node s
            #  here I converted current position into a tuple of
            #row and col, then converted this tuple to a string
            #so it can be used as a dictionary key
            closed_set[str((s.row, s.col))] = 1


            #  continue searching, create child nodes of current node
            s_row = s.row
            s_col = s.col
            for d in range(4): #letter "d" represent direction
                #letter "n" represent neighbour
                n_row = s.row + dy[d]
                n_col = s.col + dx[d]

                #  check if neighbour node is in maze bounds
                if (n_row >= 0 and n_row < self.maze_height and
                    n_col >= 0 and n_col < self.maze_width):

                    #  if this neighbour is already visited (already in
                    #closed list) discard it
                    if (str((n_row, n_col)) in closed_set):
                        continue

                    #  calculate new g and h value for the child node
                    new_g = s.g+1 if (self.maze_map[n_row][n_col] == 0) else s.g+self.OBSTACLE_COST
                    new_heuristic = self.calculate_h(n_row, n_col)


                    #  create new child node and add it to the priority queue

                    new_s = Node(s, n_row, n_col, new_g, new_heuristic)
                    Q.put(new_s, new_s.g + new_s.h)


        print("Number of obstacles along the path:", int(found_path_cost/self.OBSTACLE_COST))
        print("Found path manhattan dist:", len(found_path))
        return found_path, found_path_cost

if __name__ == "__main__":

    #
    #  MAZE 1
    #
    print("\n MAZE 1\n")
    m = [[0, 1, 1, 0], 
         [0, 0, 1, 0], 
         [1, 1, 1, 0], 
         [1, 0, 1, 0]]
    maze = Maze(m)
    maze.print_maze()

    start_row = 0
    start_col = 0
    end_row = len(m)-1
    end_col = len(m[0])-1
    maze.solve(start_row, start_col, end_row, end_col)


    #
    #  MAZE 2
    #
    print("\n MAZE 2\n")
    m = [[0, 1, 1, 0], 
         [0, 0, 1, 0], 
         [1, 1, 1, 1], 
          [1, 0, 1, 0]]
    maze = Maze(m)
    maze.print_maze()

    start_row = 0
    start_col = 0
    end_row = len(m)-1
    end_col = len(m[0])-1
    maze.solve(start_row, start_col, end_row, end_col)

Выходы:

 MAZE 1

0110
0010
1110
1010
Number of obstacles along the path: 1
Found path manhattan dist: 6

 MAZE 2

0110
0010
1111
1010
Number of obstacles along the path: 2
Found path manhattan dist: 6
...