Skip to content

A* (A-star)

Алгоритм A* (произносится как "А-звезда") - это популярный алгоритм поиска пути, который находит кратчайший путь между двумя точками на графе. Он сочетает в себе преимущества алгоритма Дейкстры (который гарантирует нахождение кратчайшего пути) и жадного поиска по первому наилучшему совпадению (который более эффективен).

Основные бласти применения

  1. Игровая разработка (поиск пути для NPC в стратегиях или RPG)
  2. Робототехника (планирование траектории движения роботов)
  3. Логистика (оптимизация доставки и складских операций, маршрутизация в GPS-навигаторах)
  4. Машинное обучение (решение задач планирования, например, в автономных дронах)
  5. Автономные транспортные средства (маршрутизация в GPS-навигаторах)

Основные принципы A*

Алгоритм A* использует эвристическую функцию для оценки стоимости пути от текущей точки до цели. Он вычисляет суммарную стоимость \(f(n)\) для каждой вершины \(n\):

\[ f(n) = g(n) + h(n) \]

Где:

  • \(g(n)\) - стоимость пути от начальной точки до вершины n
  • \(h(n)\) - эвристическая оценка стоимости от вершины n до цели

Пример реализации A* на Python

Вот реализация алгоритма A* для поиска пути на сетке:

import heapq
from typing import List, Tuple, Dict, Optional

class Node:
    def __init__(self, position: Tuple[int, int], parent: Optional['Node'] = None):
        self.position = position
        self.parent = parent
        self.g = 0  # стоимость от начала до текущей точки
        self.h = 0  # эвристическая оценка до цели
        self.f = 0  # суммарная стоимость: f = g + h

    def __eq__(self, other):
        return self.position == other.position

    def __lt__(self, other):
        return self.f < other.f

    def __repr__(self):
        return f"Node({self.position}, g={self.g}, h={self.h}, f={self.f})"

def heuristic(a: Tuple[int, int], b: Tuple[int, int]) -> float:
    """Манхэттенское расстояние между точками a и b"""
    return abs(a[0] - b[0]) + abs(a[1] - b[1])

def a_star(grid: List[List[int]], start: Tuple[int, int], end: Tuple[int, int]) -> List[Tuple[int, int]]:
    """Алгоритм A* для поиска пути на сетке

    Args:
        grid: 2D список, где 0 - проходимая клетка, 1 - препятствие
        start: координаты начальной точки (строка, столбец)
        end: координаты конечной точки (строка, столбец)

    Returns:
        Список координат пути от start до end
    """
    # Создаем начальный и конечный узлы
    start_node = Node(start)
    end_node = Node(end)

    # Инициализируем открытый и закрытый списки
    open_list = []
    closed_list = []

    # Добавляем начальный узел в открытый список
    heapq.heappush(open_list, start_node)

    # Возможные движения (вверх, вниз, влево, вправо)
    movements = [(0, -1), (0, 1), (-1, 0), (1, 0)]

    # Пока открытый список не пуст
    while open_list:
        # Извлекаем узел с наименьшей f
        current_node = heapq.heappop(open_list)

        # Если достигли цели
        if current_node == end_node:
            path = []
            current = current_node
            while current is not None:
                path.append(current.position)
                current = current.parent
            return path[::-1]  # Возвращаем путь от начала до конца

        # Добавляем текущий узел в закрытый список
        closed_list.append(current_node)

        # Генерируем соседние узлы
        for movement in movements:
            node_position = (
                current_node.position[0] + movement[0],
                current_node.position[1] + movement[1]
            )

            # Проверяем, что соседняя клетка в пределах сетки
            if (node_position[0] < 0 or node_position[0] >= len(grid) or
                node_position[1] < 0 or node_position[1] >= len(grid[0])):
                continue

            # Проверяем, что клетка проходима (не препятствие)
            if grid[node_position[0]][node_position[1]] != 0:
                continue

            # Создаем новый узел
            new_node = Node(node_position, current_node)

            # Если узел уже в закрытом списке, пропускаем
            if new_node in closed_list:
                continue

            # Вычисляем стоимости
            new_node.g = current_node.g + 1
            new_node.h = heuristic(new_node.position, end_node.position)
            new_node.f = new_node.g + new_node.h

            # Если новый узел уже в открытом списке с меньшей стоимостью, пропускаем
            if any(open_node for open_node in open_list if open_node == new_node and open_node.g <= new_node.g):
                continue

            # Добавляем новый узел в открытый список
            heapq.heappush(open_list, new_node)

    # Если путь не найден
    return []

# Пример использования
if __name__ == "__main__":
    # 0 - проходимая клетка, 1 - препятствие
    grid = [
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    ]

    start = (0, 0)
    end = (7, 6)

    path = a_star(grid, start, end)
    print("Найденный путь:")
    for position in path:
        print(position)

Визуализация пути

Для лучшего понимания можно визуализировать путь на сетке:

def print_grid_with_path(grid, path):
    for i in range(len(grid)):
        for j in range(len(grid[0])):
            if (i, j) == path[0]:
                print("S", end=" ")  # Start
            elif (i, j) == path[-1]:
                print("E", end=" ")  # End
            elif (i, j) in path:
                print("*", end=" ")  # Path
            else:
                print(grid[i][j], end=" ")
        print()

# Визуализация
print("\nВизуализация пути:")
print_grid_with_path(grid, path)

Важные замечания

  1. Эвристическая функция должна быть допустимой (не переоценивать расстояние до цели) для гарантии оптимальности. В данном примере используется манхэттенское расстояние, которое подходит для сеток с движением в 4 направлениях.

  2. Диагональное движение можно добавить, включив соответствующие смещения в список movements и изменив эвристику (например, на расстояние Чебышева).

  3. Производительность можно улучшить, используя более эффективные структуры данных для открытого списка (например, двоичную кучу, как в примере).

  4. Размер сетки - алгоритм может быть неэффективным для очень больших сеток, в таких случаях можно рассмотреть другие варианты, такие как IDA* или Jump Point Search.

Алгоритм A* широко используется в играх, робототехнике и системах навигации благодаря своей эффективности и способности находить оптимальные пути.