A* (произносится как «А-стар») — это алгоритм обхода графа и поиска пути , который используется во многих областях компьютерных наук благодаря своей полноте, оптимальности и оптимальной эффективности. [1] При наличии взвешенного графа , исходного узла и целевого узла алгоритм находит кратчайший путь (относительно заданных весов) от источника до цели.
Одним из основных практических недостатков является его пространственная сложность , где d — глубина решения (длина кратчайшего пути), а b — фактор ветвления (среднее число последователей на состояние), поскольку он хранит все сгенерированные узлы в памяти. Таким образом, в практических системах маршрутизации путешествий он, как правило, уступает алгоритмам, которые могут предварительно обрабатывать граф для достижения лучшей производительности, [2], а также подходам с ограничением памяти; однако A* по-прежнему является лучшим решением во многих случаях. [3]
Питер Харт , Нильс Нильссон и Бертрам Рафаэль из Стэнфордского исследовательского института (ныне SRI International ) впервые опубликовали алгоритм в 1968 году . [4] Его можно рассматривать как расширение алгоритма Дейкстры . A* достигает лучшей производительности, используя эвристику для управления поиском.
По сравнению с алгоритмом Дейкстры, алгоритм A* находит только кратчайший путь от указанного источника до указанной цели, а не дерево кратчайших путей от указанного источника до всех возможных целей. Это необходимый компромисс для использования эвристики, направленной на конкретную цель. Для алгоритма Дейкстры, поскольку генерируется все дерево кратчайших путей, каждый узел является целью, и не может быть эвристики, направленной на конкретную цель.
A* был создан как часть проекта Shakey , целью которого было создание мобильного робота, который мог бы планировать свои собственные действия. Нильс Нильссон изначально предложил использовать алгоритм Graph Traverser [5] для планирования пути Shakey. [6] Graph Traverser руководствуется эвристической функцией h ( n ) , оценочным расстоянием от узла n до конечного узла: он полностью игнорирует g ( n ) , расстоянием от начального узла до n . Бертрам Рафаэль предложил использовать сумму, g ( n ) + h ( n ) . [7] Питер Харт изобрел концепции, которые мы сейчас называем допустимостью и согласованностью эвристических функций. A* изначально был разработан для поиска путей с наименьшей стоимостью, когда стоимость пути является суммой его стоимостей, но было показано, что A* можно использовать для поиска оптимальных путей для любой задачи, удовлетворяющей условиям алгебры стоимости. [8]
Оригинальная статья A* 1968 года [4] содержала теорему, утверждающую, что никакой алгоритм типа A* [a] не может расширить меньше узлов, чем A*, если эвристическая функция согласована и правило разрешения конфликтов A* выбрано соответствующим образом. «Исправление» было опубликовано несколько лет спустя [9], утверждая, что согласованность не требуется, но это было показано ложным в 1985 году в окончательном исследовании оптимальности A* (теперь называемом оптимальной эффективностью) Дектера и Перла, которое дало пример A* с эвристикой, которая была допустимой, но не согласованной, расширяя произвольно больше узлов, чем альтернативный алгоритм типа A*. [10]
A* — это алгоритм информированного поиска или поиск по принципу «лучший первый» , то есть он сформулирован в терминах взвешенных графов : начиная с определенного начального узла графа, он стремится найти путь к заданному целевому узлу с наименьшей стоимостью (наименьшее пройденное расстояние, кратчайшее время и т. д.). Он делает это, поддерживая дерево путей, начинающихся в начальном узле, и расширяя эти пути по одному ребру за раз, пока не будет достигнут целевой узел.
На каждой итерации своего основного цикла A* необходимо определить, какой из своих путей следует расширить. Он делает это на основе стоимости пути и оценки стоимости, необходимой для расширения пути до цели. В частности, A* выбирает путь, который минимизирует
где n — следующий узел на пути, g ( n ) — стоимость пути от начального узла до n , а h ( n ) — эвристическая функция, которая оценивает стоимость самого дешевого пути от n до цели. Эвристическая функция специфична для проблемы. Если эвристическая функция допустима — то есть она никогда не переоценивает фактическую стоимость достижения цели — A* гарантированно вернет путь с наименьшей стоимостью от начала до цели.
Типичные реализации A* используют приоритетную очередь для выполнения повторного выбора узлов с минимальной (оцененной) стоимостью для расширения. Эта приоритетная очередь известна как открытое множество , граница или рубеж . На каждом шаге алгоритма узел с наименьшим значением f ( x ) удаляется из очереди, значения f и g его соседей обновляются соответствующим образом, и эти соседи добавляются в очередь. Алгоритм продолжается до тех пор, пока удаленный узел (таким образом, узел с наименьшим значением f из всех узлов границы) не станет целевым узлом. [b] Значение f этой цели затем также является стоимостью кратчайшего пути, поскольку h в цели равно нулю в допустимой эвристике.
Описанный до сих пор алгоритм дает только длину кратчайшего пути. Чтобы найти фактическую последовательность шагов, алгоритм можно легко пересмотреть так, чтобы каждый узел на пути отслеживал своего предшественника. После запуска этого алгоритма конечный узел будет указывать на своего предшественника, и так далее, пока предшественник некоторого узла не станет начальным узлом.
Например, при поиске кратчайшего маршрута на карте h ( x ) может представлять собой расстояние по прямой до цели, поскольку это физически наименьшее возможное расстояние между любыми двумя точками. Для карты-сетки из видеоигры использование расстояния Taxicab или расстояния Чебышева становится лучшим вариантом в зависимости от набора доступных движений (4-х или 8-ми).
Если эвристика h удовлетворяет дополнительному условию h ( x ) ≤ d ( x , y ) + h ( y ) для каждого ребра ( x , y ) графа (где d обозначает длину этого ребра), то h называется монотонной или последовательной . С последовательной эвристикой A* гарантированно найдет оптимальный путь, не обрабатывая ни один узел более одного раза, и A* эквивалентно запуску алгоритма Дейкстры с уменьшенной стоимостью d' ( x , y ) = d ( x , y ) + h ( y ) − h ( x ) . [11]
Следующий псевдокод описывает алгоритм:
функция restore_path ( cameFrom , current ) total_path := {current} пока current в cameFrom . Ключи : current := cameFrom [ current ] total_path . prepend ( current ) return total_path // A* находит путь от начала до цели. // h — эвристическая функция. h(n) оценивает стоимость достижения цели от узла n. function A_Star ( start , goal , h ) // Набор обнаруженных узлов, которые, возможно, потребуется (повторно) расширить. // Изначально известен только начальный узел. // Обычно это реализуется как min-heap или приоритетная очередь, а не как хэш-набор. openSet := {start} // Для узла n cameFrom[n] — это узел, непосредственно предшествующий ему на самом дешевом пути от начала // до n, известном в настоящее время. cameFrom := пустая карта // Для узла n gScore[n] — это известная на данный момент стоимость самого дешевого пути от начала до n. gScore := map со значением по умолчанию Infinity gScore [ start ] := 0 // Для узла n, fScore[n] := gScore[n] + h(n). fScore[n] представляет наше текущее лучшее предположение о том, // насколько дешевым может быть путь от начала до конца, если он проходит через n. fScore : = map со значением по умолчанию Infinity fScore [ start ] := h ( start ) while openSet is not empty // Эта операция может быть выполнена за время O(Log(N)) если openSet является минимальной кучей или приоритетной очередью current := узел в openSet с наименьшим значением fScore [] if current = goal return reform_path ( cameFrom , current ) openSet . Remove ( current ) для каждого соседа текущего // d(current,neighbor) - вес ребра от текущего к соседу // tentative_gScore - расстояние от начала до соседа через текущий tentative_gScore := gScore [ current ] + d ( current , neighbor ) if tentative_gScore < gScore [ neighbor ] // Этот путь к соседу лучше любого предыдущего. Запишите его! cameFrom [ neighbor ] : = current gScore [ neighbor ] := tentative_gScore fScore [ neighbor ] := tentative_gScore + h ( neighbor ) if neighbor not in openSet openSet . add ( neighbor ) // Открытый набор пуст, но цель так и не была достигнута return failure
Замечание: В этом псевдокоде, если узел достигнут одним путем, удален из openSet, а затем достигнут более дешевым путем, он будет снова добавлен в openSet. Это необходимо для гарантии того, что возвращенный путь будет оптимальным, если эвристическая функция допустима , но не согласована . Если эвристика согласована, то при удалении узла из openSet путь к нему гарантированно будет оптимальным, поэтому тест ' tentative_gScore < gScore[neighbor]
' всегда будет неудачным, если узел будет достигнут снова.
Пример алгоритма A* в действии, где узлы — это города, соединенные дорогами, а h(x) — это расстояние по прямой до целевой точки:
Обозначения: зеленый: начало; синий: цель; оранжевый: посещение
Алгоритм A* имеет реальные приложения. В этом примере ребра — это железные дороги, а h(x) — это расстояние по дуге большого круга (кратчайшее возможное расстояние на сфере) до цели. Алгоритм ищет путь между Вашингтоном, округ Колумбия, и Лос-Анджелесом.
Существует ряд простых оптимизаций или деталей реализации, которые могут существенно повлиять на производительность реализации A*. Первая деталь, которую следует отметить, заключается в том, что способ обработки приоритетной очередью связей может существенно влиять на производительность в некоторых ситуациях. Если связи разрываются, и очередь ведет себя в манере LIFO , A* будет вести себя как поиск в глубину среди путей с равной стоимостью (избегая исследования более одного одинаково оптимального решения).
Когда в конце поиска требуется путь, обычно для каждого узла сохраняется ссылка на родителя этого узла. В конце поиска эти ссылки можно использовать для восстановления оптимального пути. Если эти ссылки сохраняются, то может быть важно, чтобы один и тот же узел не появлялся в очереди приоритетов более одного раза (каждая запись соответствует другому пути к узлу, и каждая имеет свою стоимость). Стандартный подход здесь заключается в проверке того, появляется ли уже в очереди приоритетов узел, который должен быть добавлен. Если это так, то указатели приоритета и родителя изменяются, чтобы соответствовать пути с меньшей стоимостью. Стандартная двоичная куча на основе приоритетов напрямую не поддерживает операцию поиска одного из своих элементов, но ее можно дополнить хэш-таблицей , которая сопоставляет элементы с их положением в куче, позволяя выполнять эту операцию уменьшения приоритета за логарифмическое время. В качестве альтернативы куча Фибоначчи может выполнять те же операции уменьшения приоритета за постоянное амортизированное время .
Алгоритм Дейкстры , как еще один пример алгоритма поиска с равномерной стоимостью, можно рассматривать как частный случай A*, где для всех x . [12] [13] Общий поиск в глубину можно реализовать с помощью A*, учитывая, что существует глобальный счетчик C, инициализированный очень большим значением. Каждый раз, когда мы обрабатываем узел, мы назначаем C всем его вновь обнаруженным соседям. После каждого назначения мы уменьшаем счетчик C на единицу. Таким образом, чем раньше обнаружен узел, тем выше его значение. Как алгоритм Дейкстры, так и поиск в глубину можно реализовать более эффективно, не включая значение в каждом узле.
На конечных графах с неотрицательными весами ребер A* гарантированно завершается и является полным , т. е. он всегда найдет решение (путь от начала до цели), если оно существует. На бесконечных графах с конечным фактором ветвления и стоимостями ребер, которые отделены от нуля ( для некоторого фиксированного ), A* гарантированно завершается только в том случае, если существует решение. [1]
Алгоритм поиска считается допустимым, если он гарантированно возвращает оптимальное решение. Если эвристическая функция, используемая A*, допустима , то A* допустим. Интуитивное «доказательство» этого таково:
Когда A* завершает свой поиск, он находит путь от начала до цели, фактическая стоимость которого ниже, чем предполагаемая стоимость любого пути от начала до цели через любой открытый узел (значение узла ). Когда эвристика допустима, эти оценки оптимистичны (не совсем — см. следующий абзац), поэтому A* может спокойно игнорировать эти узлы, поскольку они не могут привести к более дешевому решению, чем то, которое у него уже есть. Другими словами, A* никогда не упустит возможность более дешевого пути от начала до цели, и поэтому он продолжит поиск до тех пор, пока такие возможности не исчезнут.
Фактическое доказательство немного сложнее, поскольку значения открытых узлов не гарантированно будут оптимистичными, даже если эвристика допустима. Это потому, что значения открытых узлов не гарантированно будут оптимальными, поэтому сумма не гарантированно будет оптимистичной.
Алгоритм A оптимально эффективен по отношению к набору альтернативных алгоритмов Alts на наборе задач P, если для каждой задачи P в P и каждого алгоритма A′ в Alts набор узлов, расширенный A при решении P, является подмножеством (возможно, равным) набора узлов, расширенного A′ при решении P. Окончательное исследование оптимальной эффективности A* принадлежит Рине Дехтер и Джуде Перл. [10] Они рассмотрели множество определений Alts и P в сочетании с эвристикой A*, которая является просто допустимой или является как последовательной , так и допустимой. Самый интересный положительный результат, который они доказали, заключается в том, что A* с последовательной эвристикой оптимально эффективен по отношению ко всем допустимым алгоритмам поиска типа A* для всех «непатологических» задач поиска. Грубо говоря, их понятие непатологической задачи — это то, что мы теперь подразумеваем под «до разрешения конфликта». Этот результат не выполняется, если эвристика A* допустима, но не последовательна. В этом случае Дектер и Перл показали, что существуют допустимые алгоритмы типа A*, которые могут расширять произвольно меньше узлов, чем A*, на некоторых непатологических проблемах.
Оптимальная эффективность касается набора расширенных узлов, а не количества расширений узлов (количества итераций основного цикла A*). Когда используемая эвристика допустима, но не последовательна, возможно, что узел будет расширен A* много раз, экспоненциальное число раз в худшем случае. [14] В таких обстоятельствах алгоритм Дейкстры может превзойти A* с большим отрывом. Однако более поздние исследования показали, что этот патологический случай возникает только в определенных надуманных ситуациях, когда вес ребра поискового графа экспоненциально зависит от размера графа, и что определенные непоследовательные (но допустимые) эвристики могут привести к уменьшению количества расширений узлов в поиске A*. [15] [16]
Хотя критерий допустимости гарантирует оптимальный путь решения, это также означает, что A* должен проверить все одинаково достойные пути, чтобы найти оптимальный путь. Чтобы вычислить приблизительные кратчайшие пути, можно ускорить поиск за счет оптимальности, ослабив критерий допустимости. Часто мы хотим ограничить это ослабление, чтобы гарантировать, что путь решения не хуже, чем (1 + ε ) раз от оптимального пути решения. Эта новая гарантия называется ε -допустимой.
Существует ряд ε -допустимых алгоритмов:
Временная сложность A* зависит от эвристики. В худшем случае неограниченного пространства поиска число расширенных узлов экспоненциально зависит от глубины решения (кратчайшего пути) d : O ( b d ) , где b — коэффициент ветвления (среднее число последователей на состояние). [24] Это предполагает, что целевое состояние вообще существует и достижимо из начального состояния; если это не так, а пространство состояний бесконечно, алгоритм не завершится.
Эвристическая функция оказывает большое влияние на практическую производительность поиска A*, поскольку хорошая эвристика позволяет A* отсечь многие из узлов b d , которые неинформированный поиск расширил бы. Ее качество может быть выражено в терминах эффективного коэффициента ветвления b * , который может быть определен эмпирически для экземпляра проблемы путем измерения количества узлов, сгенерированных расширением, N , и глубины решения, а затем решения [25]
Хорошие эвристики — это те, у которых эффективный коэффициент ветвления низкий (оптимальным является b * = 1 ).
Временная сложность полиномиальна, когда пространство поиска представляет собой дерево, имеется единственное целевое состояние и эвристическая функция h удовлетворяет следующему условию:
где h * — оптимальная эвристика, точная стоимость перехода от x к цели. Другими словами, ошибка h не будет расти быстрее, чем логарифм «идеальной эвристики» h * , которая возвращает истинное расстояние от x до цели. [18] [24]
Пространственная сложность A* примерно такая же, как и у всех других алгоритмов поиска по графу, поскольку он сохраняет все сгенерированные узлы в памяти. [1] На практике это оказывается самым большим недостатком поиска A*, что приводит к разработке эвристических поисков с ограничением памяти, таких как итеративное углубление A* , ограниченный памятью A* и SMA* .
A* часто используется для решения общей проблемы поиска пути в таких приложениях, как видеоигры, но изначально он был разработан как общий алгоритм обхода графа. [4] Он находит применение в различных задачах, включая задачу синтаксического анализа с использованием стохастических грамматик в обработке естественного языка . [26] Другие случаи включают информационный поиск с онлайн-обучением. [27]
Отличие A* от жадного алгоритма поиска по лучшему первому заключается в том, что он учитывает стоимость/пройденное расстояние g ( n ) .
Некоторые общие варианты алгоритма Дейкстры можно рассматривать как частный случай A*, где эвристика для всех узлов; [12] [13] в свою очередь, и Дейкстра, и A* являются частными случаями динамического программирования . [28] Сам A* является частным случаем обобщения ветвей и границ . [29]
A* похож на лучевой поиск, за исключением того, что лучевой поиск сохраняет ограничение на количество путей, которые он должен исследовать. [30]
A* также можно адаптировать к алгоритму двунаправленного поиска , но необходимо уделить особое внимание критерию остановки. [34]
Одной из первых проблем, которую мы рассмотрели, было то, как спланировать последовательность «точек маршрута», которые Шейки мог бы использовать для навигации из одного места в другое. […] Навигационная задача Шейки — это задача поиска, похожая на те, о которых я упоминал ранее.
Бертрам Рафаэль, который в то время руководил работой над Shakey, заметил, что лучшим значением для оценки была бы сумма пройденного расстояния от исходного положения плюс моя эвристическая оценка того, какое расстояние должен был пройти робот.