Мы обсудим алгоритм планирования пути A-star, как он работает, псевдокод и его реализацию с помощью python и matplotlib.
Он используется во многих областях информатики из-за полноты, оптимальности и оптимальной эффективности. В предыдущей статье мы обсуждали алгоритм Дейкстры. Чтобы получить лучшее и ясное понимание после прочтения предыдущей статьи, мы сохраним ту же терминологию, но изменим код, чтобы повысить его ясность и понимание.
Можно сказать, что A* — это расширение Дейкстры, использующее эвристические функции для повышения эффективности. Оба алгоритма используются для поиска кратчайшего пути между двумя узлами во взвешенном графе.
Сравнение алгоритма Дейкстры и алгоритма A star
Алгоритм Дейкстры предназначен для поиска кратчайшего пути от указанного исходного узла ко всем другим достижимым узлам в графе, создавая в процессе дерево кратчайших путей. Алгоритм A*, напротив, использует эвристическую функцию для направления поиска к определенному целевому узлу, что позволяет более эффективно находить кратчайший путь между указанным источником и целевым узлом. Однако это достигается за счет того, что A* не генерирует все дерево кратчайших путей, что означает, что он не может найти кратчайший путь ко всем возможным целям. Поэтому выбор алгоритма зависит от конкретных требований решаемой задачи.
Что такое эвристика и зачем нам ее использовать?
Эвристики — это стратегии или методы решения проблем, которые используют практический опыт и здравый смысл для поиска решения. Они используются в информатике для улучшения процессов решения проблем, делая их более эффективными и действенными.
В контексте алгоритма A* эвристики используются для направления процесса поиска к целевому состоянию. Он использует комбинацию фактической стоимости для достижения узла от начального узла (известного как G) и расчетной стоимости от текущего узла до цели (известного как H), чтобы определить, какой узел посетить следующим. Общая стоимость узла равна сумме G и H или F = G + H.
Они помогают алгоритму принимать обоснованные решения о том, какие узлы исследовать дальше в пространстве поиска.
Допустимая эвристика никогда не завышает фактическую стоимость достижения цели.
Непротиворечивая эвристика удовлетворяет условию, что оценочная стоимость от текущего узла до целевого узла всегда меньше или равна оценочному расстоянию от любой соседней вершины до цели плюс стоимость достижения этого соседа.
Cost(a -> c) <= Cost(a -> b) + Cost(b -> c)
Хорошая эвристика для алгоритма A* допустима и непротиворечива.
Теперь мы кратко обсудим F = G+H
F = G + H
G
Это то же самое, что мы называем стоимостью в алгоритме Дейкстры. Там написано, на сколько единиц мы переместились из начального узла. Это не дает интуитивного представления о том, где находится целевой узел. Его даже не волнует, где находится целевой узел.
H
Он дает оценку того, где находится целевой узел. Обычно используемыми эвристическими функциями являются евклидово расстояние, манхэттенское расстояние, расстояние Чебышева, октильное расстояние и т. д. Мы должны убедиться, что стоимость никогда не завышена.
Пользовательские функции могут быть использованы в зависимости от ситуации. Например, во время путешествия может быть доступно несколько вариантов маршрута, и обычно учитываются такие факторы, как условия движения, безопасность дорожного движения и другие важные факторы, в качестве полезных эвристик, помогающих в принятии решений при выборе маршрута.
F
В конце концов, общая стоимость представляет собой сумму как расчетной стоимости от начального узла до текущего узла, так и расчетной стоимости от целевого узла до текущего узла.
псевдокод
Давайте напишем код в реальном времени на простом примере, чтобы понять, как работает A* и эвристика
Код очень похож на алгоритм Дейкстры. Здесь мы в основном фокусируемся на эвристическом расчете и добавляем его к стоимости при поиске кратчайшего узла.
Путешествуя по дороге, мы видим разные пути, каждый из которых имеет разные эвристики, давайте рассмотрим аналогичную ситуацию и решим ее. заштрихованная часть в два раза больше эвристики, чем остальные.
зеленый узел => Начальный узел (10,45)
синий узел => Целевой узел (90,45)
черные точки =› препятствия
голубая зона пересечения => Это грубый путь, стоимость которого высока. (20,0) => (80,50). У нас есть 2 разных эвристики для грубого пути, стоимость которого больше.
Если нет концепции разных путей, кратчайший путь - это прямая линия, это не проблема, алгоритм Дейкстры выводит прямую линию, поскольку он не учитывает эвристику. Теперь мы увидим, как алгоритм A-star находит кратчайший путь.
import matplotlib.pyplot as plt import math show_animation = True class AStarPlanner: def __init__(self, ox, oy, resolution, robot_radius): self.x_min = 0 self.y_min = 0 self.x_max = 0 self.y_max = 0 self.x_width = 0 self.y_width = 0 self.obstacle_map = 0 self.resolution = resolution self.robot_radius = robot_radius self.calc_obstacle_map(ox, oy) self.motion = self.get_motion_model() class Node: def __init__(self, x, y, cost, parent_index): self.x = x # index of grid self.y = y # index of grid self.cost = cost self.parent_index = parent_index # index of previous Node def __str__(self): return str(self.x) + "," + str(self.y) + "," + str(self.cost) + "," + str(self.parent_index) def planning(self,sx,sy,gx,gy): start_node = self.Node(self.calc_xy_index(sx, self.x_min),self.calc_xy_index(sy, self.y_min), 0.0, -1) goal_node = self.Node(self.calc_xy_index(gx, self.x_min),self.calc_xy_index(gy, self.y_min), 0.0, -1) open_set, closed_set = dict(), dict() open_set[self.calc_index(start_node)] = start_node print(str(start_node)) while True: if len(open_set) == 0: print("Open set is empty..") break curr_id = min(open_set, key=lambda o: open_set[o].cost + self.calc_heuristic(goal_node,open_set[o])) current = open_set[curr_id] if current.x == goal_node.x and current.y == goal_node.y: print("Find Goal") goal_node.parent_index = current.parent_index goal_node.cost = current.cost break del open_set[curr_id] closed_set[curr_id] = current for i, _ in enumerate(self.motion): node = self.Node(current.x + self.motion[i][0], current.y + self.motion[i][1], current.cost + self.motion[i][2], curr_id) n_id = self.calc_index(node) if n_id in closed_set: continue if not self.verify_node(node): continue if n_id not in open_set: open_set[n_id] = node else: if open_set[n_id].cost > node.cost: open_set[n_id] = node rx,ry = self.calc_final_path(goal_node,closed_set) # returns distance return rx,ry def calc_position(self,index,minp): pos = index*self.resolution + minp return pos def calc_final_path(self,goal_node,closed_set): rx,ry = [self.calc_position(goal_node.x ,self.x_min)], [self.calc_position(goal_node.y,self.y_min)] parent_index = goal_node.parent_index print(goal_node.cost) while parent_index != -1: n = closed_set[parent_index] rx.append(self.calc_position(n.x,self.x_min)) ry.append(self.calc_position(n.y,self.y_min)) parent_index = n.parent_index return rx,ry def calc_index(self, node): return (node.y - self.y_min) * self.x_width + (node.x - self.x_min) def calc_xy_index(self,position,minp): return round((position - minp)/self.resolution) @staticmethod def calc_heuristic(n1,n2): if n2.x >= 20 and n2.x <= 80 and n2.y <= 50: d = max(abs(n1.x-n2.x) ,abs(n1.y-n2.y))*2 else: d = max(abs(n1.x-n2.x) ,abs(n1.y-n2.y)) return d def verify_node(self,node): px = self.calc_position(node.x,self.x_min) py = self.calc_position(node.y,self.y_min) if px < self.x_min: return False if py < self.y_min: return False if px >= self.x_max: return False if py >= self.y_max: return False if self.obstacle_map[node.x][node.y]: return False return True def calc_obstacle_map(self,ox,oy): self.x_min = round(min(ox)) self.x_max = round(max(ox)) self.y_min = round(min(oy)) self.y_max = round(max(oy)) self.x_width = round((self.x_max - self.x_min) / self.resolution) self.y_width = round((self.y_max - self.y_min) / self.resolution) self.obstacle_map = [[False for _ in range(self.y_width)] for _ in range(self.x_width)] for ix in range(self.x_width): x = self.calc_position(ix,self.x_min) for iy in range(self.y_width): y = self.calc_position(iy,self.y_min) for iox,ioy in zip(ox,oy): d = math.hypot(iox-x,ioy -y) if d <= self.robot_radius: self.obstacle_map[ix][iy] = True break @staticmethod def get_motion_model(): #dx,dy,cost motion = [[1,0,1],[0,1,1],[-1,0,1],[0,-1,1],[-1,-1,math.sqrt(2)], [-1,1,math.sqrt(2)],[1,-1,math.sqrt(2)],[1,1,math.sqrt(2)]] return motion def main(): print( __file__ + "start!!" ) # start and goal position sx = 10.0 #[m] sy = 45.0 #[m] gx = 90.0 #[m] gy = 45.0 #[m] grid_size = 1.0 robot_radius = 1.0 # set obstacle positions ox,oy =[],[] hx,hy = [],[] for i in range(20,81,10): for j in range(0,51,10): hx.append(i) hy.append(j) for i in range(0,101): ox.append(i) oy.append(0.0) for i in range(0,101): ox.append(i) oy.append(100.0) for i in range(0,101): ox.append(100.0) oy.append(i) for i in range(0,101): ox.append(0.0) oy.append(i) for i in range(30,70): ox.append(i) oy.append(50) if show_animation: plt.plot(ox,oy, ".k") plt.plot(sx,sy, "og") plt.plot(hx,hy, "-b") plt.plot(gx,gy, "xb") plt.grid(True) plt.axis("equal") a_star = AStarPlanner(ox, oy, grid_size, robot_radius) rx, ry = a_star.planning(sx, sy, gx, gy) if show_animation: # pragma: no cover plt.plot(rx, ry, "-r") plt.pause(0.001) plt.show() if __name__ == '__main__': main()
Класс узла содержит x, y (индексы), стоимость и родительский индекс. Каждая точка сетки считается узлом.
Работающий
Инициализируйте начальный узел и целевой узел. Создайте 2 набора open_set для хранения исследуемых узлов и Closed_set для хранения самых коротких узлов.
curr_id хранит индекс кратчайшего узла в open_set в начале каждой итерации. current содержит кратчайший узел.
Эвристика добавляется при сравнении стоимости для поиска кратчайшего узла. когда мы добавляем затраты при перемещении, мы не учитываем эвристику предыдущего узла.
Наносится текущая позиция алгоритма. события клавиатуры перехватываются и запускаются для выхода из программы, если нажата клавиша escape.
Когда целевой узел достигнут, цикл прерывается. Если нет, цикл продолжается
ближайший узел удаляется из open_set и добавляется в Closed_Set.
Для каждого движения мы находим соответствующий узел и находим его индекс. Если узел уже находится в closed_set, он уже исследован и это был самый короткий узел, поэтому, если мы снова исследуем тот же узел, это означает, что мы попадем в цикл, поэтому мы должны остановить итерацию и перейти к следующей итерации. Проверьте узел, если он не удовлетворяет заданным ограничениям, затем остановите итерацию и перейдите к следующей итерации.
Если нового узла нет в open_set, добавьте его. Если он уже присутствует, сравните стоимость, если новая стоимость меньше, чем текущий путь является кратчайшим, поэтому замените узел (это похоже на замену родительского индекса и стоимости).
Теперь все новые узлы добавляются в open_list, и в новой итерации мы находим ближайший узел в существующем open_set, и поток продолжается……
Наконец, в конце концов, итерации проходят через target_node и Closed_Set, чтобы найти окончательный путь.
Мы обсудим функцию, используемую в
calc_heuristic-›Чтобы понять, как двигаться к желаемой конечной точке, мы можем использовать эвристику, которая может быть сформулирована на основе определенных критериев.
def calc_heuristic(n1,n2): # n1 => goal node n2 => current node if n2.x >= 20 and n2.x <= 80 and n2.y <= 50: d = max(abs(n1.x-n2.x) ,abs(n1.y-n2.y))*2 else: d = max(abs(n1.x-n2.x) ,abs(n1.y-n2.y)) return d
Общая эвристика:
Евклидово расстояние -> sqrt((x2 — x1)² + (y2 — y1)²)
Манхэттенское расстояние -> |x2 — x1| + |у2 — у1|
Расстояние Чебышева -> max(|x2 — x1|, |y2 — y1|)
Октильное расстояние -> max(|x2 — x1|, |y2 — y1|) + (sqrt(2) — 1) * min(|x2 — x1|, |y2 — y1|)
calc_position – преобразует индекс в метры (положение).
calc_xy_index -> Преобразует метры (положение) в индекс в сетке
calc_index -> Когда мы добавляем узлы в open_set и Closed_Set, для каждого узла должен быть уникальный идентификатор (уникальный индекс при сохранении узлов). Мы можем использовать любое выражение, уникальное для всех узлов или координат.
verify_node -›когда мы перемещаемся, нам нужно проверить, находится ли перемещенный узел в заданных ограничениях или нет. Оно не должно быть ни препятствием, ни границей, ни переходить границы.
calc_obstacle_map -›Он принимает позиции препятствий в качестве входных данных и преобразует их в индексы и блокирует узлы от прохождения через него.
get_motion_model -›Содержит возможное движение и добавленную стоимость из текущего узла.
calc_final_path ->Теперь у нас есть target_node (index) и closed_set. преобразовать target_node из index в position. повторите его через родительский индекс и преобразуйте в позицию и добавьте его.
Из функции calc_final_path мы получаем конечные позиции пути и наносим их на график.
Когда вся сетка имеет одинаковую эвристику, то очевидно прямой маршрут является кратчайшим путем, который мы можем видеть в алгоритме Дейкстры. Когда регион имеет другую эвристику, стоимость проезда через него увеличивается, поэтому она корректируется соответствующим образом.
GitHub: https://github.com/vamsi1961