Как повысить эффективность алгоритма поиска по звездочке? - PullRequest
0 голосов
/ 28 мая 2020

У меня есть сетка в matplotlib (20 * 20 или 40 * 40 в зависимости от выбора пользователя), которая содержит данные, разделенные на основе местоположения LatLong. Каждая ячейка в этой сетке представляет собой область 0,002 или 0,001 (например: от [-70.55, 43.242] до [-70.548, 43.244]). Сетка окрашивается в зависимости от порогового значения, скажем, все, что выше 30, является зеленым, а все, что ниже - красным.

Я реализовал алгоритм запуска A для go из одного места (ячейки) в другое на этом графике, избегая при этом все зеленые ячейки. Стоимость проезда по границе зеленой и красной ячейки составляет 1,3, а стоимость по диагонали - 1,5, а путешествие между двумя красными ячейками - 1.

Я использую heuristi c по диагонали, и для каждой ячейки я получаю всех возможных соседей и устанавливаю их значение G на основе порога.

Теперь я могу получить правильный путь большую часть времени, а для соседних ячеек он работает на sub 1 se c. но когда я go дальше проходит 14-18 секунд.

Я не понимаю, что я здесь делаю не так? Я пытался найти способ улучшить его, но не смог.

Вот фрагмент алгоритма. Я хотел бы отметить, что определение доступных соседей и установка значения G здесь может не быть проблемой, потому что каждый вызов функции выполняется примерно за 0,02 - 0,03 секунды.

Любой совет приветствуется! Спасибо

def a_star(self, start, end):
    startNode = Node(None, start)
    endNode = Node(None, end)
    openList = []
    closedList = []
    openList.append(startNode)

    while len(openList) > 0:

        current = openList[0]
        if current.location == endNode.location:
            path = []
            node = current
            while node is not None:
                path.append(node)
                node = node.parent
            return path[::-1]

        openList.pop(0)
        closedList.append(current)

       # This takes around 0.02 0.03 seconds
        neighbours = self.get_neighbors(current)

        for neighbour in neighbours:
            append = True
            for node in closedList:
                if neighbour.location == node.location:
                    append = False
                    break
            for openNode in openList:
                if neighbour.location == openNode.location:
                    if neighbour.g < openNode.g:
                        append = False
                        openNode.g = neighbour.g
                        break
            if append:

                neighbour.h = round(self.heuristic(neighbour.location, endNode.location), 3)
                neighbour.f = round(neighbour.g + neighbour.h, 3)

                bisect.insort_left(openList, neighbour)
    return False

Редактировать: Добавить фрагмент узла

 class Node:
    def __init__(self, parent, location):
        self.parent = parent
        self.location = location
        self.g = 0
        self.h = 0
        self.f = 0

Редактировать 2: Добавить изображение

circle is start location, star is destination. Yellow cells are not accessible, so no diagonal path on yellow cells, and cannot travel between two yellow cells.

Круг - начальная точка, звездочка - конечная точка. Желтые ячейки недоступны, поэтому нет диагонального пути по желтым ячейкам и не может проходить между двумя желтыми ячейками.

Ответы [ 2 ]

1 голос
/ 28 мая 2020

Как уже указывал @ leni c, внутренние циклы, которые у вас есть, не принадлежат алгоритму A *.

Первый (for node in closedList) должен быть заменен проверкой того, узел находится в наборе (а не в списке): таким образом вам не потребуется итерация.

Второй узел (openNode in openList) не нужен, когда вы инициализируете все значения свойств g бесконечностью (кроме начальный узел). Затем вы можете просто сравнить новое значение g с тем, которое уже хранится в узле neighbor.

Кроме того, я бы предложил создавать узлы для всего графа сразу после его создания. Это будет полезно, когда вам нужно выполнить несколько запросов на одном и том же графике.

Кроме того, вместо bisect.insort_left я бы предложил использовать heapq.heappush. Это будет быстрее, так как на самом деле он не сортирует очередь полностью, а просто обеспечивает сохранение свойства кучи. Однако временная сложность такая же. Что еще более важно, временная сложность получения следующего значения из него лучше, чем openList.pop(0).

Я бы посоветовал работать со стоимостью 10, 13 и 15 вместо 1, 1,3 и 1,5. , поскольку у целочисленных операций нет проблем с точностью.

По той же причине я не стал бы работать с дробными координатами местоположения. Поскольку все они так же далеко друг от друга (например, 0,002), мы можем просто использовать последовательные целые числа (0, 1, 2, ...) для обеих координат. Дополнительная функция может принимать решение и опорную пару координат для преобразования этих целочисленных координат обратно в «мировые» координаты.

Я сделал несколько предположений:

  • Это строго запрещено go через "враждебные" ячейки. Для их пересечения не будет никаких ребер. Например, если начальная локация находится в середине четырех враждебных ячеек, пути не будет. Можно было бы рассмотреть альтернативу, в которой эти края будут иметь чрезвычайно высокую стоимость, чтобы вы всегда могли предложить путь.

  • Прямые края на границе сетки - это все разрешается. Их стоимость будет 1,3 (13), когда соседняя с ней одна ячейка является враждебной. Таким образом, на самом деле в каждом из двух измерений есть на одно местоположение больше, чем количество ячеек

  • Пороговый вход будет дробью от 0 до 1, что указывает на долю ячеек, которая должна быть дружественный по отношению к общему количеству ячеек, и это значение будет преобразовано в "разделенное" значение, чтобы различать guish между дружественными и враждебными ячейками.

Вот код, который вы могли бы использовать в качестве вдохновения:

from heapq import heappop, heappush

class Node:
    def __init__(self, location):
        self.location = location
        self.neighbors = []
        self.parent = None
        self.g = float('inf')
        self.f = 0

    def clear(self):
        self.parent = None
        self.g = float('inf')
        self.f = 0

    def addneighbor(self, cost, other):
        # add edge in both directions
        self.neighbors.append((cost, other))
        other.neighbors.append((cost, self))

    def __gt__(self, other):  # make nodes comparable
        return self.f > other.f

    def __repr__(self):
        return str(self.location)

class Graph:
    def __init__(self, grid, thresholdfactor):
        # get value that corresponds with thresholdfactor (which should be between 0 and 1)
        values = sorted([value for row in grid for value in row])
        splitvalue = values[int(len(values) * thresholdfactor)]
        print("split at ", splitvalue)
        # simplify grid values to booleans and add extra row/col of dummy cells all around
        width = len(grid[0]) + 1
        height = len(grid) + 1
        colors = ([[False] * (width + 1)] +
            [[False] + [value < splitvalue for value in row] + [False] for row in grid] +
            [[False] * (width + 1)])

        nodes = []
        for i in range(height):
            noderow = []
            nodes.append(noderow)
            for j in range(width):
                node = Node((i, j))
                noderow.append(node)
                cells = [colors[i+1][j]] + colors[i][j:j+2]  # 3 cells around location: SW, NW, NE
                for di, dj in ((1, 0), (0, 0), (0, 1), (0, 2)):  # 4 directions: W, NW, N, NE
                    cost = 0
                    if (di + dj) % 2:  # straight
                        # if both cells are hostile, then not allowed
                        if cells[0] or cells[1]:  # at least one friendly cell
                            # if one is hostile, higher cost
                            cost = 13 if cells[0] != cells[1] else 10
                        cells.pop(0)
                    elif cells[0]:  # diagonal: cell must be friendly
                        cost = 15
                    if cost:
                        node.addneighbor(cost, nodes[i-1+di][j-1+dj])
        self.nodes = nodes

    @staticmethod
    def reconstructpath(node):
        path = []
        while node is not None:
            path.append(node)
            node = node.parent
        path.reverse()
        return path

    @staticmethod
    def heuristic(a, b):
        # optimistic score, assuming all cells are friendly
        dy = abs(a[0] - b[0])
        dx = abs(a[1] - b[1])
        return min(dx, dy) * 15 + abs(dx - dy) * 10

    def clear(self):
        # remove search data from graph 
        for row in self.nodes:
            for node in row:
                node.clear()

    def a_star(self, start, end):
        self.clear()
        startnode = self.nodes[start[0]][start[1]]
        endnode = self.nodes[end[0]][end[1]]
        startnode.g = 0
        openlist = [startnode] 
        closed = set()
        while openlist:
            node = heappop(openlist)
            if node in closed:
                continue
            closed.add(node)
            if node == endnode:
                return self.reconstructpath(endnode)
            for weight, neighbor in node.neighbors:
                g = node.g + weight
                if g < neighbor.g:
                    neighbor.g = g
                    neighbor.f = g + self.heuristic(neighbor.location, endnode.location)
                    neighbor.parent = node
                    heappush(openlist, neighbor)

Я закодировал график, который вы включили в качестве изображения, чтобы увидеть, как будет вести себя код:

grid = [
    [38, 32, 34, 24,  0, 82,  5, 41, 11, 32,  0, 16,  0,113, 49, 34, 24,  6, 15, 35],
    [61, 61,  8, 35, 65, 31, 53, 25, 66,  0, 21,  0,  9,  0, 31, 75, 20,  8,  3, 29],
    [43, 66, 47,114, 38, 41,  1,108,  9,  0,  0,  0, 39,  0, 27, 72, 19, 14, 24, 25],
    [45,  5, 37, 23,102, 25, 49, 34, 41, 49, 35, 15, 29, 21, 66, 67, 44, 31, 38, 91],
    [47, 94, 48, 69, 33, 95, 18, 75, 28, 70, 38, 78, 48, 88, 21, 66, 44, 70, 75, 23],
    [23, 84, 53, 23, 92, 14, 71, 12,139, 30, 63, 82, 16, 49, 76, 56,119,100, 47, 21],
    [30,  0, 32, 90,  0,195, 85, 65, 18, 57, 47, 61, 40, 32,109,255, 88, 98, 39,  0],
    [ 0,  0,  0,  0, 39, 39, 76,167, 73,140, 58, 56, 94, 61,212,222,141, 50, 41, 20],
    [ 0,  0,  0,  5,  0,  0, 21,  2,132,100,218, 81,  0, 62,135, 42,131, 80, 14, 19],
    [ 0,  0,  0,  0,  0, 15,  9, 55, 70, 71, 42,117, 65, 63, 59, 81,  4, 40, 77, 46],
    [ 0,  0,  0,  0, 55, 52,101, 93, 30,166, 56, 19, 76,103, 54, 37, 24, 23, 59, 98],
    [ 0,  0,  0,  0,  9,  9, 44,149, 11,134, 90, 64, 44, 57, 61, 79,270,201, 84,  6],
    [ 0,  0,  0, 22,  1, 15,  0, 25, 30,101,154, 60, 97, 64, 15,162, 27, 91, 71,  0],
    [ 0,  0,  1, 35,  5, 10,  0, 55, 25,  0,200, 81, 31, 53, 42, 74,127,154,  7,  0],
    [ 0,  0,187, 17, 45, 66, 91,191, 70,189, 18, 25, 67, 32, 40, 79,103, 79, 59,  0],
    [ 0, 21, 16, 14, 19, 58,278, 56,128, 95,  3, 52,  9, 27, 25, 43, 62, 25, 38,  0],
    [ 4,  3, 11, 26,119,165, 53, 85, 46, 81, 19, 11, 12, 19, 18,  9, 16,  6, 37,  0],
    [ 5,  0,  0, 65,158,153,118, 38,123, 46, 28, 24,  0, 21, 11, 20,  5,  1, 10,  0],
    [17,  4, 28, 81,101,101, 46, 25, 44, 12, 41,  6, 27,  8,  4, 32, 40,  1,  1,  0],
    [26, 20, 84, 42,112, 27, 14, 16,  5, 13,  3, 43,  6, 18, 12, 44,  5,  0,  0,  5]
]

graph = Graph(grid, 0.5) # provide the threshold at initialisation
path = graph.a_star((7, 4), (14, 18))
print(path)
1 голос
/ 28 мая 2020

Эта часть очень неэффективна. Для каждого соседа, который вы проходите по двум относительно большим спискам, это очень быстро увеличивает общую сложность, когда списки начинают расти:

for node in closedList:
    if neighbour.location == node.location:
        append = False
        break
for openNode in openList:
    if neighbour.location == openNode.location:

В принципе, ваш алгоритм не должен зависеть ни от каких списков. У вас есть ячейки, вы выбираете одну из списка, вы получаете 8 соседей, вы обрабатываете их, сравнивая с имеющимися у вас ячейками, а затем некоторые из них добавляются в список. Нет необходимости вводить oop над списками.

...